微信小程序 & uni-app实现简单的即时通讯(IM)

背景介绍:

一直以来,将即时通讯(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。

结尾声明:

衷心感谢每一位读者的耐心阅读与观看。作为初次尝试撰写文章的作者,我深知还有许多不足与待提升之处。对于大家在阅读过程中提出的宝贵问题和建议,我将认真倾听、悉心吸收,并在未来的文章中不断改进和完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值