背景介绍:
一直以来,将即时通讯(IM)功能集成到项目中都是一件不太简单的事情,这往往意味着需要承担高昂的第三方服务费用,或是面对繁琐复杂的文档资料。对于小程序开发者而言,引入腾讯IM更可能因资源包体积过大而面临无法顺利发布至线上的困境。本文旨在简化前端开发者在项目中嵌入IM的流程,使其变得更为便捷高效,希望能够为大家带来实质性的帮助。
准备阶段
Vue3版本的 uniapp 模版,node.js 版本建议 18 以上,我用的是 v18.19.0
HBuilder X 版本号为 4.08
微信开发者工具为 2024 年 5 月版本
开发过程
首先我们在 App.vue 写一个判断登录方法,放到 onLaunch 里面去执行,用户登录之后会在本地存储一个 token 如果没有获取到说明用户就没有登录,那我们就不用创建 WebSocket 的长连接。
由于开发过程中注释大部分被删掉(为了给小程序的发布留出更多的空间),本文中的代码注释基本由字节的豆包 MarsCode AI 生成,里面有许多功能还是不错的,推荐大家也去使用一下。
App.vue 代码片段
// 定义一些状态变量
const state = reactive({
token: null,
heartTimer: null,
serverTimer: null,
reconnectTimer: null,
sendFixHeartTimer: null,
isOpen: false,
pingIntervalSeconds: 3000,
LockReconnect: false,
unreadChatList: []
})
onLaunch(() => {
uni.getStorage({
key: 'token',
success: ({ data }) => {
state.token = data;
LoginImMethods();
},
fail: (error) => {
// 处理获取失败情况, 目前为空
}
})
});
// 登录方法
const LoginImMethods = () => {
// 清除本地存储中的用户消息数量
uni.removeStorageSync('UserMessageNum')
// 清除本地存储中的聊天标志
uni.removeStorageSync('chatFlag')
// 清除本地存储中的用户未读聊天列表
uni.removeStorageSync('UserUnreadChatList')
// 清除本地存储中的聊天对象ID
uni.removeStorageSync('chatVid')
// 清除本地存储中的提示跟踪对象
uni.removeStorageSync('tipsTrackerObj')
// 如果用户未登录,则显示授权模态框
if (!sheep.$store('user').isLogin) {
showAuthModal();
console.log('未登录')
return;
} else {
// 如果用户已登录,则执行IM登录逻辑
console.log('已登录,执行IM')
setTimeout(() => {
// 如果用户信息中存在id,则进行WebSocket连接
if (sheep.$store('user').userInfo.id) {
// 连接WebSocket服务器
uni.connectSocket({
// 在url里填写上后端提供的WebSocket地址, 这里给大家一个示例
url: `wss://xxx.xxx.xxx/websocket `
});
// 监听WebSocket连接打开事件
uni.onSocketOpen(function (res) {
// 发送登录请求
sendJump({ code: 1 });
console.log('WebSocket连接已打开');
// 开启心跳
startHeartbeat()
// 开启定时发送心跳
sendFixHeart()
});
// 监听WebSocket消息事件
uni.onSocketMessage(function (e) {
// 如果消息代码不等于0,则处理消息
if (JSON.parse(e.data).code!= 0) {
// 如果消息发送者不是当前用户,则处理消息
if (JSON.parse(e.data).message.fromId!= sheep.$store('user').userInfo.id) {
// console.log("收到消息")
const _data = JSON.parse(e.data);
// 获取聊天标志
uni.getStorage({
key: 'chatFlag',
success: (res) => {
// 如果聊天标志为关闭,则不添加消息到本地
if (res.data == 'close') {
// console.log("用户点击底部消息,存储状态改变,停止添加消息至本地")
}
},
fail: (err) => {
// console.log(err)
// 将消息添加到未读聊天列表
state.unreadChatList.push(_data.message)
// 更新本地存储中的未读聊天列表
uni.setStorage({
key: 'UserUnreadChatList',
data: JSON.stringify(state.unreadChatList)
});
// 更新本地存储中的用户消息数量
uni.setStorage({
key: 'UserMessageNum',
data: state.unreadChatList.length
});
}
})
// 传递消息
uni.$emit('contentIM', {
content: _data.message
})
}
}
});
// 监听WebSocket连接关闭事件
uni.onSocketClose(function (res) {
console.log('WebSocket 已关闭!');
// 重连
reconnect()
});
// 监听WebSocket连接错误事件
uni.onSocketError(function (res) {
console.log('WebSocket连接打开失败,请检查');
// 重连
reconnect()
});
}
}, 300)
}
}
// 为了防止构建的 WebSocket 长连接断连,我们需要采用心跳机制来解决这个问题
// 开启心跳
const startHeartbeat = () => {
// 清除之前的心跳定时器(如果存在)
state.heartTimer && clearTimeout(state.heartTimer);
// 设置一个新的心跳定时器,每隔 state.pingIntervalSeconds 秒发送一个心跳包(code: 0)
state.heartTimer = setInterval(() => {
sendJump({ code: 0 });
}, state.pingIntervalSeconds * 1000); // 确保pingIntervalSeconds是秒数
}
// 重连方法
const reconnect = () => {
// 如果当前处于重连锁定状态,则直接返回
if (state.lockReconnect) return;
// 设置重连锁定状态,以避免重复重连
state.lockReconnect = true;
// 清除之前的重连定时器(如果存在)
state.reconnectTimer && clearTimeout(state.reconnectTimer);
// 设置一个新的重连定时器,在随机延迟(3000 到 5000 毫秒)后尝试重新连接
state.reconnectTimer = setTimeout(() => {
// 从本地存储中获取 token
uni.getStorage({
key: 'token',
success: ({ data }) => {
// 将获取到的 token 赋值给 state.token
state.token = data;
// 调用 LoginImMethods 函数进行登录
LoginImMethods()
},
fail: (error) => { }
})
// 重连成功后,解除重连锁定状态
state.lockReconnect = false;
}, parseInt(Math.random() * 2000 + 3000));
}
// 2s 固定发送心跳
const sendFixHeart = () => {
// 清除之前的固定心跳定时器(如果存在)
clearInterval(state.sendFixHeartTimer);
// 设置一个新的固定心跳定时器,每隔 2 秒发送一个心跳包(code: 0)
state.sendFixHeartTimer = setInterval(() => {
sendJump({
code: 0,
});
}, 2000);
}
// 心跳发送包
const sendJump = (data) => {
// 将给定的数据对象转换为 JSON 字符串
const json = JSON.stringify(data)
// 通过 uni.sendSocketMessage 方法发送 JSON 数据到 WebSocket 服务器
uni.sendSocketMessage({
data: json,
success: function () {
},
fail: function (err) {
console.log(err);
}
});
}
消息列表页 & 消息详情页的开发
目前两个页面大致如下
消息列表页面部分代码片段
这里我只列出 HTML 和 JS 部分代码片段, CSS 样式大家可以参考杨涛大佬的开源作品,链接在下面我已经挂出来了
这里首先感谢插件市场中杨哥提供的UI界面,(聊天消息列表(好友列表+私人单聊)支持App、H5、小程序 - DCloud 插件市场), 在此基础上经过稍微修改,帮我节省了构建页面的时间,能让我有大把精力放到页面的 js 部分。
这里的 zf-leftOperation-box 也是插件市场的一个插件,用来滑动删除信息(滑动删除 - DCloud 插件市场),
其中为了业务需要我也对组件内部进行了小修改,也附在下面的代码中。
HTML 部分
<template>
<!-- 如果 state.users 长度不为 0,则显示页面 -->
<view class="page" v-if="state.users.length!= 0">
<!-- 左操作框组件,用于显示聊天列表和操作选项 -->
<zf-leftOperation-box :styles='styles' ref='moveRef' :option='option' :list="state.users" @callBack="callBack" :tipsTracker="tipsTracker">
<!-- 定义一个插槽,用于自定义每个聊天项的左侧内容 -->
<template #left="{item}">
<view class="conList">
<!-- 头像部分 -->
<view class="avatar">
<!-- 显示用户头像,如果没有则显示默认头像 -->
<image
:src="item.avatar? item.avatar : '默认头像地址'"
mode="aspectFill"></image>
</view>
<!-- 聊天内容部分 -->
<view class="content">
<!-- 标题部分,包含用户名和时间 -->
<view class="title">
<!-- 显示用户名 -->
<text class="name">{{ item.name }}</text>
<!-- 显示时间,使用 formatRelativeToNow 函数格式化时间 -->
<text class="time">{{ formatRelativeToNow(item.time)}}</text>
</view>
<!-- 标题底部,包含消息内容和未读提示 -->
<view class="title-bottom">
<!-- 显示消息内容,如果没有则显示默认提示 -->
<view class="txt" v-if="item.msg">{{ item.msg }}</view>
<view class="txt" v-if="!item.msg">[链接或图片信息, 详情请点击查看]</view>
<!-- 显示未读提示,如果有未读消息则显示数量,否则不显示 -->
<text class="tips" v-if="item.tips">{{ item.tips > 99? '99+' : item.tips }}</text>
</view>
</view>
</view>
</template>
</zf-leftOperation-box>
</view>
<!-- 如果 state.users 长度为 0,则显示空消息提示 -->
<s-empty v-if="state.users.length == 0"
icon="图片地址"
text="暂无消息" />
</template>
JavaScript 部分
// 变量存储
const state = reactive({
users: [],
vids: [],
tips: 0,
params: {
pageSize: 10,
pageNo: 1
},
unreadArr: [],
tipsTrackerObj: {},
newMessage: null
});
// 存储每个用户的tips
const tipsTracker = {};
// 存储总未读消息数
let totalUnreadTips = 0;
// 自定义zf-leftOperation-box中zf-left-item盒子的样式
const styles = {
width: '750rpx',
display: 'flex',
padding: '30rpx 0',
}
// 滑动按钮的配置
const option = [{
text: '删除',
bg: '#F56C6C',
type: 'del'
}]
const moveRef = ref(null)
// 定义一个回调函数,用于处理滑动操作的结果
// callBack是组件方法稍作修改,具体用法可以去插件市场查看组件文档
const callBack = (e) => {
// 获取渲染的list并做相应的操作
if (e.type == "del") {
// 将指定 vid 的 tips 重置为 0
tipsTracker[e.vid] = 0;
// 更新 state.users 中对应 vid 的 tips 为 0
state.users.forEach((item) => {
if (item.vid == e.vid) {
item.tips = 0;
}
});
// 重新计算总未读消息数
totalUnreadTips = Object.values(tipsTracker).reduce((sum, value) => sum + value, 0);
// 更新本地存储中的 UserMessageNum
uni.setStorage({
key: 'UserMessageNum',
data: totalUnreadTips
});
// 更新本地存储中的 tipsTrackerObj
uni.setStorage({
key: 'tipsTrackerObj',
data: JSON.stringify(tipsTracker)
});
// 显示删除成功的提示
uni.showToast({
title: '删除成功',
icon: 'none'
});
// 从 moveRef.value.list 中过滤掉指定 id 的项
moveRef.value.list = moveRef.value.list.filter(item => item.id!= e.id);
// 更新 state.users
state.users = moveRef.value.list;
} else {
// 显示修改成功的提示
uni.showToast({
title: '修改成功',
icon: 'none'
});
}
// 调用 moveRef.value.creatInit() 方法
moveRef.value.creatInit();
};
// 获取聊天好友 这个方法和下面的方法大致一样, 只是此方法用来首次调用
function chatFriends() {
// 调用 ImIndexDiscussListApi 获取聊天消息列表
ImIndexDiscussListApi.getChatMessageList().then((res) => {
// 将返回的数据赋值给 state.users
state.users = res.data;
// 从返回的数据中提取每个用户的 vid,并存入 state.vids
state.vids = res.data.map(item => item.vid);
// 对每个 vid 发起请求,获取聊天消息历史(最近100条)
const chatRequests = state.vids.map((vid) => {
return ImIndexDiscussListApi.getChatMessageHistory({
chatId: vid,
fromId: sheep.$store('user').userInfo.id,
type: 0,
pageSize: 100
}).then((chatRes) => {
// 只返回最后一条消息内容(本来想传 1 获取最后一条, 后端说 pageSize 不能传1)
return chatRes.data[chatRes.data.length - 1];
}).catch((error) => {
// 处理请求错误
console.log(error);
return null;
});
});
// 等待所有请求完成
Promise.all(chatRequests).then((allChatData) => {
// 添加聊天记录最后一条消息信息
state.users.forEach((item, index) => {
item.msg = allChatData[index].content;
item.time = allChatData[index].timestamp;
});
// 从本地存储获取未读消息列表并更新用户tips
uni.getStorage({
key: 'UserUnreadChatList',
success: (res) => {
// 将获取到的数据解析并赋值给 state.unreadArr
state.unreadArr = JSON.parse(res.data);
// 计算总未读消息数量
totalUnreadTips = state.unreadArr.length;
// 更新每个用户的未读消息数量
updateTipsWithUnreadList(state.users, state.unreadArr);
// 从本地存储中获取 tipsTrackerObj
uni.getStorage({
key: 'tipsTrackerObj',
success: (res) => {
// 将获取到的数据解析并赋值给 state.tipsTrackerObj
state.tipsTrackerObj = JSON.parse(res.data);
// 计算总未读消息数量
totalUnreadTips = Object.values(state.tipsTrackerObj).reduce((sum, value) => sum + value, 0);
// 更新每个用户的未读消息数量
state.users.forEach((item) => {
if (state.tipsTrackerObj.hasOwnProperty(item.vid)) {
item.tips = state.tipsTrackerObj[item.vid];
} else {
item.tips = 0;
}
});
},
fail: (err) => {
// 处理获取 tipsTrackerObj 失败的情况
}
});
},
fail: (err) => {
// 处理获取 UserUnreadChatList 失败的情况
}
});
// 根据 tips 和 time 对用户列表进行排序
state.users.sort((a, b) => {
// 首先比较 tips
if (b.tips!== a.tips) {
return b.tips - a.tips;
}
// 如果 tips 相同,则比较 time
return b.time - a.time;
});
// 打印更新后的用户列表
console.log('更新后的用户列表:', state.users);
}).catch((error) => {
// 处理 Promise.all 的错误
});
});
}
// 获取聊天好友
function chatFriendShow() {
// 调用 ImIndexDiscussListApi 获取聊天消息列表
ImIndexDiscussListApi.getChatMessageList().then((res) => {
// 将返回的数据赋值给 state.users
state.users = res.data;
// 从返回的数据中提取每个用户的 vid,并存入 state.vids
state.vids = res.data.map(item => item.vid);
// 对每个 vid 发起请求,获取最近100条聊天消息
const chatRequests = state.vids.map((vid) => {
return ImIndexDiscussListApi.getChatMessageHistory({
chatId: vid,
fromId: sheep.$store('user').userInfo.id,
type: 0,
pageSize: 100
}).then((chatRes) => {
// 只返回最后一条消息内容 (这里问了后端 pageSize 不能传1)
return chatRes.data[chatRes.data.length - 1];
}).catch((error) => {
// 处理请求错误
console.log(error);
return null;
});
});
// 等待所有请求完成
Promise.all(chatRequests).then((allChatData) => {
// 添加聊天记录最后一条消息信息
state.users.forEach((item, index) => {
item.msg = allChatData[index].content;
item.time = allChatData[index].timestamp;
});
// 从本地存储中获取 tipsTrackerObj
uni.getStorage({
key: 'tipsTrackerObj',
success: (res) => {
// 将获取到的数据解析并赋值给 state.tipsTrackerObj
state.tipsTrackerObj = JSON.parse(res.data);
// 计算总未读消息数量
totalUnreadTips = Object.values(state.tipsTrackerObj).reduce((sum, value) => sum + value, 0);
// 更新每个用户的未读消息数量
state.users.forEach((item) => {
if (state.tipsTrackerObj.hasOwnProperty(item.vid)) {
item.tips = state.tipsTrackerObj[item.vid];
} else {
item.tips = 0;
}
});
},
fail: (err) => {
// console.log(err);
}
});
// 根据 tips 和 time 对用户列表进行排序
state.users.sort((a, b) => {
// 首先比较 tips
if (b.tips!== a.tips) {
return b.tips - a.tips;
}
// 如果 tips 相同,则比较 time
return b.time - a.time;
});
}).catch((error) => {
// 处理 Promise.all 的错误
});
});
}
// 监听 WebSocket 消息推送
uni.$on('contentIM', (data) => {
state.newMessage = data.content;
updateTipsWithNewMessage(state.users, state.newMessage);
})
// 更新用户的 tips 和总未读消息数(基于新消息)
function updateTipsWithNewMessage(users, message) {
uni.getStorage({
key: 'chatVid',
success: (res) => {
if (res.data == message.fromId) {
// console.log("当前用户聊天页面, 不再进行消息提示")
} else {
updateTipsUserMessage(users, message)
}
},
fail: (err) => {
updateTipsUserMessage(users, message)
}
})
}
// 消息提醒公共函数
function updateTipsUserMessage(users, message) {
// 遍历用户列表
users.forEach((item) => {
// 如果用户的 vid 与消息的 fromId 相等
if (item.vid === message.fromId) {
// 如果 tipsTracker 中没有该用户的 vid,则初始化为 0
if (!tipsTracker[item.vid]) {
tipsTracker[item.vid] = 0;
}
// 获取之前的未读消息数量
const previousTips = tipsTracker[item.vid];
// 增加新的未读消息数量
const newTips = previousTips + 1;
// 更新 tipsTracker 中的未读消息数量
tipsTracker[item.vid] = newTips;
// 更新用户的未读消息数量
item.tips = newTips;
// 更新用户的最后一条消息内容和时间戳
item.msg = message.content;
item.time = message.timestamp;
}
});
// 计算总未读消息数量
totalUnreadTips = Object.values(tipsTracker).reduce((sum, value) => sum + value, 0);
// 将 tipsTracker 存储到本地缓存中
uni.setStorage({
key: 'tipsTrackerObj',
data: JSON.stringify(tipsTracker)
});
// 更新本地缓存中的总未读消息数量
uni.setStorage({
key: 'UserMessageNum',
data: totalUnreadTips
});
}
// 从本地存储获取未读消息列表并更新用户tips
function updateTipsWithUnreadList(users, unreadList) {
// 统计未读消息列表中每个用户的未读消息数量
const unreadTips = countFromIds(unreadList);
// 遍历用户列表
users.forEach((item) => {
// 如果未读消息列表中存在该用户的未读消息数量
if (unreadTips.hasOwnProperty(item.vid)) {
// 更新用户的未读消息数量
item.tips = unreadTips[item.vid];
// 如果 tipsTracker 中没有该用户的未读消息数量,则初始化为当前值
if (!tipsTracker[item.vid]) {
tipsTracker[item.vid] = unreadTips[item.vid];
}
} else {
// 如果未读消息列表中不存在该用户的未读消息数量,则设置为 0
item.tips = 0;
}
});
// 将 tipsTracker 存储到本地缓存中
uni.setStorage({
key: 'tipsTrackerObj',
data: JSON.stringify(tipsTracker)
});
}
// 筛选本地单个对话消息数量
function countFromIds(messages) {
// 初始化一个对象,用于存储每个用户的未读消息数量
const countMap = {};
// 遍历消息列表
messages.forEach(message => {
// 获取消息的发送者 ID
const fromId = message.fromId;
// 如果 countMap 中已经存在该用户的未读消息数量,则加 1
if (countMap[fromId]) {
countMap[fromId]++;
} else {
// 如果 countMap 中不存在该用户的未读消息数量,则初始化为 1
countMap[fromId] = 1;
}
});
// 返回统计结果
return countMap;
}
// 时间处理函数
function formatRelativeToNow(timestamp) {
// 获取当前时间的时间戳
const now = new Date().getTime();
// 计算时间差,单位为毫秒
const diffMs = now - timestamp;
// 将时间差转换为天数
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
// 根据时间差的天数来格式化时间字符串
if (diffDays === 0) {
// 如果是今天,返回小时和分钟
const date = new Date(timestamp);
const hours = date.getHours();
const minutes = date.getMinutes();
return `今天${hours < 10? '0' : ''}${hours}:${minutes < 10? '0' : ''}${minutes}`;
} else if (diffDays === 1) {
// 如果是一天前,返回“一天前”
return '一天前';
} else if (diffDays === 2) {
// 如果是两天前,返回“两天前”
return '两天前';
} else if (diffDays === 3) {
// 如果是三天前,返回“三天前”
return '三天前';
} else if (diffDays === 4) {
// 如果是四天前,返回“四天前”
return '四天前';
} else if (diffDays === 5) {
// 如果是五天前,返回“五天前”
return '五天前';
} else if (diffDays === 6) {
// 如果是六天前,返回“六天前”
return '六天前';
} else if (diffDays === 7) {
// 如果是七天前,返回日期(月/日)
const date = new Date(timestamp);
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month < 10? '0' : ''}${month}月${day < 10? '0' : ''}${day}号`;
} else if (diffDays > 7) {
// 如果是七天前之前,返回日期(年/月/日)
const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}年${month < 10? '0' : ''}${month}月${day < 10? '0' : ''}${day}号`;
}
// 如果时间戳无效,返回“未知时间”
return '未知时间';
}
onLoad(() => {
chatFriends()
setTimeout(() => {
// 改变消息存储状态,
// 为什么改变状态,开发中遇到了一个问题就是聊天页面并不是首页,
// 首次进入并没有加载 Dom, 所以在 App.vue 第 80 行写了一个方法获取一下状态
uni.setStorage({
key: 'chatFlag',
data: 'close'
});
}, 200)
});
onShow(() => {
uni.getStorage({
key: 'chatFlag',
success: (res) => {
if (res.data == 'close') {
chatFriendShow()
}
},
fail: (err) => {
}
})
})
zf-leftOperation-box 组件内部调整代码片段
HTML 部分
<view class="zf-left-item" :style="{...prop.styles}" v-for="(item,index) in list" :key="index" >
<view class="zf-out-box"
:style="{left:chooseIndex==index?(-(Array.isArray(item?.option)?item.option.length:prop.option.length)*120+'rpx'):'0rpx'}"
:data-index="index" @touchstart="moveStart" @touchmove="touchMove" @touchend="moveEnd">
<view class="zf-left-left zf-items" @click="connect(item)">
<slot name="left" :item="item"></slot>
</view>
<view v-if="true" class="zf-left-right zf-items"
:style="{width:(Array.isArray(item?.option)?item.option.length:prop.option.length)*120+'rpx'}"
:data-index="index">
<view class="zf-button" v-for="(it,ind) in getBtn(item?.option)" :key="index"
@tap="clickOption(item,it)" :style="{backgroundColor:it.bg}">{{it.text}}
</view>
</view>
</view>
</view>
JavaScript 部分
const prop = defineProps({
styles: {
type: Object,
default: () => {}
},
list: {
type: Array,
default: () => []
},
tipsTracker: {
type:Object,
default: () => {}
},
// 按钮配置
option: {
type: Array,
default: () => {
return [{
text: '删除',
bg: '#F56C6C',
type: 'del'
}]
}
}
})
watch(() => prop.list, (nel) => {
creatInit()
list.value = [...nel]
}, {
deep: true,
immediate: true
})
let totalUnreadTips = 0;
// 跳转到聊天详情
const connect = (item) => {
// 存储会话 vid
uni.setStorage({
key: 'chatVid',
data: item.vid
})
// 找到该用户的tips并归零
if (prop.tipsTracker[item.vid]) {
const previousTips = prop.tipsTracker[item.vid];
console.log(previousTips)
totalUnreadTips = Object.values(prop.tipsTracker).reduce((sum, value) => sum + value, 0);
prop.tipsTracker[item.vid] = 0;
item.tips = 0;
// 从总未读消息数中减去该用户的未读消息数
totalUnreadTips -= previousTips;
console.log(prop.tipsTracker)
uni.setStorage({
key: 'tipsTrackerObj',
data: JSON.stringify(prop.tipsTracker)
})
console.log("剩余未读消息数量: " + totalUnreadTips)
// 存储更新后的总未读消息数
uni.setStorage({
key: 'UserMessageNum',
data: totalUnreadTips
});
uni.navigateTo({
url: `/pages/im/chatMessage?fromStatus=chat&item=${JSON.stringify(item)}`
});
} else {
uni.navigateTo({
url: `/pages/im/chatMessage?fromStatus=chat&item=${JSON.stringify(item)}`
});
}
}
消息详情页面部分代码片段
HTML 部分
<!-- 消息容器,根据消息的用户类型设置不同的样式 -->
<view class="message" :class="[item.userType]" v-for="(item, index) in state.data.chatHistory"
:key="index">
<!-- 好友头像 -->
<view class="friend-avatar">
<!-- 如果是好友消息,显示头像 -->
<image
:src="item.avatar? item.avatar : '默认头像'"
v-if="item.userType === 'friend'" class="avatar" mode="aspectFill"
>
</image>
</view>
<!-- 图片加文本 -->
<view class="content productLink" v-if="item.bookUrl" @click="magnify(item)">
<view class="pro-left">
<image class="pro-img" :src="item.bookUrl" mode="widthFix"></image>
</view>
<view class="pro-right">
<view class="pro-text">
{{ item.bookName }}
</view>
</view>
</view>
<!-- 纯图片 -->
<view class="content productLink" v-if="item.picUrl" @click="magnify(item)">
<view class="pro-left">
<image class="pro-img" :src="item.picUrl" mode="widthFix"></image>
</view>
</view>
<!-- 文本消息 -->
<view class="content" v-if="item.msg">
{{ item.msg }}
</view>
<!-- 自己的头像 -->
<view class="self-avatar">
<!-- 如果是自己的消息,显示头像 -->
<image
:src="item.avatar? item.avatar : '默认头像'"
v-if="item.userType === 'self'" class="avatar" mode="aspectFill"
>
</image>
</view>
</view>
JavaScript 部分
// 变量存储
const state = reactive({
onChatId: null,
content: '',
data: {
chatHistory: []
},
imageArr: [],
chatName: null,
avatar:null,
})
// 发送纯文本消息
const sendFun = () => {
// 如果消息内容为空,则提示用户不能发送空消息,并返回
if (!state.content) {
uni.showToast({
icon: 'none',
title: '不能发送空消息',
duration: 1500
});
return;
}
// 如果消息内容不为空,则执行以下操作
if (state.content) {
// 将消息内容转换为 JSON 格式 (后端要求 JSON 字符串格式)
const json = JSON.stringify({
code: "2",
message: {
messageType: "0", // 后端规定文字的 messageType 为 0,图片则为 1
chatId: state.onChatId,
fromId: sheep.$store('user').userInfo.id,
mine: true,
timestamp: timestamp(),
content: state.content,
avatar: sheep.$store('user').userInfo.avatar,
type: "0",
nickname: sheep.$store('user').userInfo.nickname
}
});
// 通过 uni.sendSocketMessage 方法发送消息
uni.sendSocketMessage({
data: json,
success: function () {
// console.log('数据发送成功');
// 将发送的消息添加到聊天记录中
state.data.chatHistory.push({
avatar: sheep.$store('user').userInfo.avatar,
msg: state.content,
userType: 'self',
timestamp: timestamp(),
});
// 清空输入框内容
state.content = '';
},
fail: function (err) {
// 如果发送失败,则提示用户系统错误
uni.showToast({
icon: 'none',
title: '系统错误...请稍后',
duration: 1000
});
}
});
return;
}
};
// 发送图片, 最多支持六张
const chooseImage = () => {
uni.chooseImage({
count: 6, // 最多选择6张图片
sizeType: ['original', 'compressed'], // 可以选择原图或压缩图
sourceType: ['album'], // 从相册选择图片
success: function (res) {
const tempFilePaths = res.tempFilePaths;
const imageUrls = []; // 用于存储上传后图片的URL
// 遍历所有选择的图片并上传
Promise.all(tempFilePaths.map(item => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://xxx.xxx.com/upload', // 上传图片的接口地址(示例)
filePath: item,
name: 'file',
formData: {
'user': 'test'
},
success: (uploadFileRes) => {
// data字段为图片URL
const uploadedUrl = JSON.parse(uploadFileRes.data).data;
imageUrls.push(uploadedUrl);
resolve();
},
fail: reject
});
});
})).then(() => {
// 所有图片上传成功后执行
const messages = imageUrls.map(url => ({
code: "2",
message: {
avatar: sheep.$store('user').userInfo.avatar,
messageType: "1",
chatId: state.onChatId,
fromId: sheep.$store('user').userInfo.id,
mine: true,
timestamp: timestamp(),
content: state.content, // 输入框内容
extend: {
url: url
},
type: "0",
nickname: sheep.$store('user').userInfo.nickname
}
}));
// 发送 WebSocket 消息
messages.forEach(message => {
const data = JSON.stringify(message);
uni.sendSocketMessage({
data: data,
success: function () {
uni.showLoading({
title: '发送中'
});
setTimeout(() => {
uni.hideLoading();
// 更新聊天历史
state.data.chatHistory.push({
avatar: sheep.$store('user').userInfo.avatar,
picUrl: message.message.extend.url, // 只添加当前图片的URL
userType: 'self'
});
}, 500);
},
fail: function (err) {
console.log(err);
uni.showToast({
icon: 'none',
title: '系统错误...请稍后',
duration: 1000
});
}
});
});
}).catch(error => {
console.error('上传图片失败:', error);
uni.showToast({
icon: 'none',
title: '上传图片失败',
duration: 2000
});
});
}
});
}
// 预览聊天记录中的图片, 目前只支持单图片预览
const magnify = (item) => {
// console.log(item)
if (item.picUrl) {
// 先置空, 后赋值
state.imageArr = []
state.imageArr.push(item.picUrl)
uni.previewImage({
urls: state.imageArr,
current: 0,
success: (result) => {
},
fail: (error) => {
}
})
}
}
onLoad((options) => {
// 解析 options 中的 item,获取 chatName
state.chatName = JSON.parse(options.item).name;
// 解析 options 中的 item,获取 onChatId
state.onChatId = JSON.parse(options.item).vid;
// 解析 options 中的 item,获取 avatar
state.avatar = JSON.parse(options.item).avatar;
// 调用 ImIndexDiscussListApi 获取聊天消息历史
ImIndexDiscussListApi.getChatMessageHistory({
chatId: state.onChatId,
fromId: sheep.$store('user').userInfo.id,
type: 0,
pageSize: 100
}).then((res) => {
// 获取聊天历史数据
const chatHistory = res.data;
// 遍历聊天历史,处理每个消息
chatHistory.forEach((item) => {
// 如果消息是自己发送的(接口返回的对象类型的数组,每一项中有个mine状态, true为自己所发)
if (item.mine) {
// 设置消息类型为 self
item.userType = 'self';
// 设置消息
item.msg = item.content;
// 设置图片
item.picUrl = item.extend?.url;
} else {
// 如果消息不是自己发送的,设置消息类型为 friend
item.userType = 'friend';
// 设置消息
item.msg = item.content;
// 设置图片
item.picUrl = item.extend?.url;
}
});
// 将处理后的聊天历史数据保存到 state 中
state.data.chatHistory = chatHistory;
});
// 监听 WebSocket 消息
uni.$on('contentIM', (data) => {
// 从本地存储中获取 chatVid
const chatVid = uni.getStorageSync('chatVid');
// 获取接收到的消息内容
const message = data.content;
// 打印 chatVid 和消息发送者的 ID
console.log(chatVid);
console.log(message.fromId);
// 如果 chatVid 等于消息发送者的 ID
if (chatVid == message.fromId) {
// 更新当前发送者的 chatId
state.onChatId = message.fromId;
// 更新发送者的名字
state.chatName = message.nickname;
// 根据消息类型进行不同的处理
if (message.messageType == 0) {
// 如果是纯文本消息,将其添加到聊天历史中
state.data.chatHistory.push({
avatar: message.avatar,
msg: message.content,
userType: 'friend',
});
} else {
// 发送的图片信息
state.data.chatHistory.push({
avatar: message.avatar,
userType: 'friend',
picUrl: message.extend.url,
});
}
}
});
});
onUnload(() => {
uni.removeStorageSync('chatVid')
uni.removeStorageSync('UserUnreadChatList')
})
另外大家在退出登录时一定要关闭 WebSocket 的长连接,uni-app官方文档中已经给出方法 uni.closeSocket。
结尾声明:
衷心感谢每一位读者的耐心阅读与观看。作为初次尝试撰写文章的作者,我深知还有许多不足与待提升之处。对于大家在阅读过程中提出的宝贵问题和建议,我将认真倾听、悉心吸收,并在未来的文章中不断改进和完善。