赞
踩
本小程序在用户浏览首页的时候创建WebSocket连接,并将连接获得的WebSocket对象存储到全局变量中,方便其他页面来使用WebSocket
首先在项目的main.js文件中声明全局变量socket
Vue.prototype.$socket = null
后面初始化Websocket连接的时候对全局变量进行赋值,赋值完成之后,后续如果需要使用全局变量,直接使用this.$socket
即可
Vue.prototype.$socket = this.$socket;
下面的代码中有一个headbeat方法,该方法主要用来定时给WebSocket服务器发送一个信号,告诉WebSocket服务器当前客户端还处于连接状态。当心跳停止的时候(比如客户端断网),后端服务就会将用户信息从连接中移除
/** * 创建websocket连接 */ initWebsocket() { // console.log("this.socket:" + JSON.stringify(this.$socket)) // this.$socket == null,刚刚进入首页,还没有建立过websocket连接 // this.$socket.readyState==0 表示正在连接当中 // this.$socket.readyState==1 表示处于连接状态 // this.$socket.readyState==2 表示连接正在关闭 // this.$socket.readyState==3 表示连接已经关闭 if (this.$socket == null || (this.$socket.readyState != 1 && this.$socket.readyState != 0)) { this.$socket = uni.connectSocket({ url: "ws://10.23.17.146:8085/websocket/" + uni.getStorageSync("curUser").userName, success(res) { console.log('WebSocket连接成功', res); }, }) // console.log("this.socket:" + this.$socket) // 监听WebSocket连接打开事件 this.$socket.onOpen((res) => { console.log("websocket连接成功") Vue.prototype.$socket = this.$socket; // 连接成功,开启心跳 this.headbeat(); }); // 连接异常 this.$socket.onError((res) => { console.log("websocket连接出现异常"); // 重连 this.reconnect(); }) // 连接断开 this.$socket.onClose((res) => { console.log("websocket连接关闭"); // 重连 this.reconnect(); }) } }, /** * 重新连接 */ reconnect() { console.log("重连"); // 防止重复连接 if (this.lockReconnect == true) { return; } // 锁定,防止重复连接 this.lockReconnect = true; // 间隔一秒再重连,避免后台服务出错时,客户端连接太频繁 setTimeout(() => { this.initWebsocket(); }, 1000) // 连接完成,设置为false this.lockReconnect = false; }, // 开启心跳 headbeat() { console.log("websocket心跳"); var that = this; setTimeout(function() { if (that.$socket.readyState == 1) { // websocket已经连接成功 that.$socket.send({ data: JSON.stringify({ status: "ping" }) }) // 调用启动下一轮的心跳 that.headbeat(); } else { // websocket还没有连接成功,重连 that.reconnect(); } }, that.heartbeatTime); },
<template> <view class="container"> <scroll-view @scrolltolower="getMoreChatUserVo"> <view v-for="(chatUserVo,index) in chatUserVoList" :key="index" @click="trunToChat(chatUserVo)"> <view style="height: 10px;"></view> <view class="chatUserVoItem"> <view style="display: flex;align-items: center;"> <uni-badge class="uni-badge-left-margin" :text="chatUserVo.unReadChatNum" absolute="rightTop" size="small"> <u--image :showLoading="true" :src="chatUserVo.userAvatar" width="50px" height="50px" :fade="true" duration="450"> <view slot="error" style="font-size: 24rpx;">加载失败</view> </u--image> </uni-badge> </view> <view style="margin: 10rpx;"></view> <view style="line-height: 20px;width: 100%;display: flex;justify-content: space-between;flex-direction: column;"> <view style="display: flex; justify-content: space-between;"> <view> <view class="nickname">{{chatUserVo.userNickname}} </view> <view class="content">{{chatUserVo.lastChatContent}}</view> </view> <view class="date">{{formatDateToString(chatUserVo.lastChatDate)}}</view> </view> <!-- <view style="height: 10px;"></view> --> <u-line></u-line> </view> </view> </view> </scroll-view> </view> </template> <script> import { listChatUserVo } from "@/api/market/chat.js"; import { listChat } from "@/api/market/chat.js" export default { data() { return { chatUserVoList: [], page: { pageNum: 1, pageSize: 15 }, } }, created() { }, methods: { /** * 滑动到底部,自动加载新一页的数据 */ getMoreChatUserVo() { this.page.pageNum++; this.listChatUserVo(); }, listChatUserVo() { listChatUserVo(this.page).then(res => { // console.log("res:"+JSON.stringify(res.rows)) // this.chatUserVoList = res.rows; for (var i = 0; i < res.rows.length; i++) { this.chatUserVoList.push(res.rows[i]); } }) }, /** * 格式化日期 * @param {Object} date */ formatDateToString(dateStr) { let date = new Date(dateStr); // 今天的日期 let curDate = new Date(); if (date.getFullYear() == curDate.getFullYear() && date.getMonth() == curDate.getMonth() && date .getDate() == curDate.getDate()) { // 如果和今天的年月日都一样,那就只显示时间 return this.toDoubleNum(date.getHours()) + ":" + this.toDoubleNum(date.getMinutes()); } else { // 如果年份一样,就只显示月日 return (curDate.getFullYear() == date.getFullYear() ? "" : (date.getFullYear() + "-")) + this .toDoubleNum(( date .getMonth() + 1)) + "-" + this.toDoubleNum(date.getDate()); } }, /** * 如果传入的数字是两位数,直接返回; * 否则前面拼接一个0 * @param {Object} num */ toDoubleNum(num) { if (num >= 10) { return num; } else { return "0" + num; } }, /** * 转到私聊页面 */ trunToChat(chatUserVo) { let you = { avatar: chatUserVo.userAvatar, nickname: chatUserVo.userNickname, username: chatUserVo.userName } uni.navigateTo({ url: "/pages/chat/chat?you=" + encodeURIComponent(JSON.stringify(you)) }) }, /** * 接收消息 */ receiveMessage() { this.$socket.onMessage((response) => { // console.log("接收消息:" + response.data); let message = JSON.parse(response.data); // 收到消息,将未读消息数量加一 for (var i = 0; i < this.chatUserVoList.length; i++) { if (this.chatUserVoList[i].userName == message.from) { this.chatUserVoList[i].unReadChatNum++; // 显示对方发送的最新消息 listChat(message.from, { pageNum: 1, pageSize: 1 }).then(res => { this.chatUserVoList[i].lastChatContent = res.rows[0].content }); break; } } }) }, }, onLoad(e) { this.receiveMessage(); }, onShow: function() { this.chatUserVoList = []; this.listChatUserVo(); }, } </script> <style lang="scss"> .container { padding: 20rpx; .chatUserVoItem { display: flex; margin: 0 5px; .nickname { font-weight: 700; } .content { color: #A7A7A7; font-size: 14px; /* 让消息只显示1行,超出的文字内容使用...来代替 */ overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .date { color: #A7A7A7; font-size: 12px; } } // .uni-badge-left-margin { // margin-left: 10px; // } } </style>
当最近的一条聊天内容太长的时候,页面不太美观,缺少整齐的感觉
解决的方式非常简单,只需要添加以下样式即可
.content {
/* 让消息只显示1行,超出的文字内容使用...来代替 */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
本文显示日期时间的时候,遵循以下原则:
时:分
月-日
年-月-日
在显示月、日、时、分的时候,如果数字是一位数字
,就在前面补一个零,具体操作如方法toDoubleNum
/** * 格式化日期 * @param {Object} date */ formatDateToString(dateStr) { let date = new Date(dateStr); // 今天的日期 let curDate = new Date(); if (date.getFullYear() == curDate.getFullYear() && date.getMonth() == curDate.getMonth() && date .getDate() == curDate.getDate()) { // 如果和今天的年月日都一样,那就只显示时间 return this.toDoubleNum(date.getHours()) + ":" + this.toDoubleNum(date.getMinutes()); } else { // 如果年份一样,就只显示月日 return (curDate.getFullYear() == date.getFullYear() ? "" : (date.getFullYear() + "-")) + this .toDoubleNum(( date .getMonth() + 1)) + "-" + this.toDoubleNum(date.getDate()); } }, /** * 如果传入的数字是两位数,直接返回; * 否则前面拼接一个0 * @param {Object} num */ toDoubleNum(num) { if (num >= 10) { return num; } else { return "0" + num; } },
未读消息数量显示使用角标组件,即uni-badge
,使用该组件需要下载安装插件,下载链接,下载之前需要看广告,哈哈哈,当然有钱可以不看
显示效果如下图
<uni-badge class="uni-badge-left-margin" :text="chatUserVo.unReadChatNum" absolute="rightTop"
size="small">
<u--image :showLoading="true" :src="chatUserVo.userAvatar" width="50px" height="50px"
:fade="true" duration="450">
<view slot="error" style="font-size: 24rpx;">加载失败</view>
</u--image>
</uni-badge>
【微信公众平台模拟的手机界面】
【手机端,键盘呼出之后的聊天区域】
<template> <view style="height:100vh;"> <!-- @scrolltoupper:上滑到顶部执行事件,此处用来加载历史消息 --> <!-- scroll-with-animation="true" 设置滚动条位置的时候使用动画过渡,让动作更加自然 --> <scroll-view :scroll-into-view="scrollToView" scroll-y="true" class="messageListScrollView" :style="{height:scrollViewHeight}" @scrolltoupper="getHistoryChat()" :scroll-with-animation="!isFirstListChat" ref="chatScrollView"> <view v-for="(message,index) in messageList" :key="message.id" :id="`message`+message.id" style="width: 750rpx;min-height: 60px;"> <view style="height: 10px;"></view> <view v-if="message.type==0" class="messageItemLeft"> <view style="width: 8px;"></view> <u--image :showLoading="true" :src="you.avatar" width="50px" height="50px" radius="3"></u--image> <view style="width: 7px;"></view> <view class="messageContent left"> {{message.content}} </view> </view> <view v-if="message.type==1" class="messageItemRight"> <view class="messageContent right"> {{message.content}} </view> <view style="width: 7px;"></view> <u--image :showLoading="true" :src="me.avatar" width="50px" height="50px" radius="3"></u--image> <view style="width: 8px;"></view> </view> </view> </scroll-view> <view class="messageSend"> <view class="messageInput"> <u--textarea v-model="messageInput" placeholder="请输入消息内容" autoHeight> </u--textarea> </view> <view style="width:5px"></view> <view class="commmitButton" @click="send()">发 送</view> </view> </view> </template> <script> import { getUserProfileVo } from "@/api/user"; import { listChat } from "@/api/market/chat.js" let socket; export default { data() { return { webSocketUrl: "", socket: null, messageInput: '', // 我自己的信息 me: {}, // 对方信息 you: {}, scrollViewHeight: undefined, messageList: [], // 底部滑动到哪里 scrollToView: '', page: { pageNum: 1, pageSize: 15 }, isFirstListChat: true, loadHistory: false, // 消息总条数 total: 0, } }, created() { this.me = uni.getStorageSync("curUser"); }, beforeDestroy() { console.log("执行销毁方法"); this.endChat(); }, onLoad(e) { // 设置初始高度 this.scrollViewHeight = `calc(100vh - 20px - 44px)`; this.you = JSON.parse(decodeURIComponent(e.you)); uni.setNavigationBarTitle({ title: this.you.nickname, }) this.startChat(); this.listChat(); this.receiveMessage(); }, onReady() { // 监听键盘高度变化,以便设置输入框的高度 uni.onKeyboardHeightChange(res => { let keyBoardHeight = res.height; console.log("keyBoardHeight:" + keyBoardHeight); this.scrollViewHeight = `calc(100vh - 20px - 44px - ${keyBoardHeight}px)`; this.scrollToView = ''; setTimeout(() => { this.scrollToView = 'message' + this.messageList[this .messageList.length - 1].id; }, 150) }) }, methods: { /** * 发送消息 */ send() { if (this.messageInput != '') { let message = { from: this.me.userName, to: this.you.username, text: this.messageInput } // console.log("this.socket.send:" + this.$socket) // 将组装好的json发送给服务端,由服务端进行转发 this.$socket.send({ data: JSON.stringify(message) }); this.total++; let newMessage = { // code: this.messageList.length, type: 1, content: this.messageInput }; this.messageList.push(newMessage); this.messageInput = ''; this.toBottom(); } }, /** * 开始聊天 */ startChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "start" } // 告诉服务端要开始聊天了 this.$socket.send({ data: JSON.stringify(message) }); }, /** * 结束聊天 */ endChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "end" } // 告诉服务端要结束聊天了 this.$socket.send({ data: JSON.stringify(message) }); }, /** * 接收消息 */ receiveMessage() { this.$socket.onMessage((response) => { // console.log("接收消息:" + response.data); let message = JSON.parse(response.data); let newMessage = { // code: this.messageList.length, type: 0, content: message.text }; this.messageList.push(newMessage); this.total++; // 让scroll-view自动滚动到最新的数据那里 // this.$nextTick(() => { // // 滑动到聊天区域最底部 // this.scrollToView = 'message' + this.messageList[this // .messageList.length - 1].id; // }); this.toBottom(); }) }, /** * 查询对方和自己最近的聊天数据 */ listChat() { return new Promise((resolve, reject) => { listChat(this.you.username, this.page).then(res => { for (var i = 0; i < res.rows.length; i++) { this.total = res.total; if (res.rows[i].fromWho == this.me.userName) { res.rows[i].type = 1; } else { res.rows[i].type = 0; } // 将消息放到数组的首位 this.messageList.unshift(res.rows[i]); } if (this.isFirstListChat == true) { // this.$nextTick(function() { // // 滑动到聊天区域最底部 // this.scrollToView = 'message' + this.messageList[this // .messageList.length - 1].id; // }) this.toBottom(); this.isFirstListChat = false; } resolve(); }) }) }, /** * 滑到最顶端,分页加一,拉取更早的数据 */ getHistoryChat() { // console.log("获取历史消息") this.loadHistory = true; if (this.messageList.length < this.total) { // 当目前的消息条数小于消息总量的时候,才去查历史消息 this.page.pageNum++; this.listChat().then(() => {}) } }, /** * 滑动到聊天区域最底部 */ toBottom() { // 让scroll-view自动滚动到最新的数据那里 this.scrollToView = ''; setTimeout(() => { // 滑动到聊天区域最底部 this.scrollToView = 'message' + this.messageList[this .messageList.length - 1].id; }, 150) } } } </script> <style lang="scss"> .messageListScrollView { background: #F5F5F5; overflow: auto; .messageItemLeft { display: flex; align-items: flex-start; justify-content: flex-start; .messageContent { max-width: calc(750rpx - 10px - 50px - 15px - 10px - 50px - 15px); padding: 10px; // margin-top: 10px; border-radius: 7px; font-family: sans-serif; // padding: 10px; // 让view只包裹文字 width: auto; // display: inline-block !important; // display: inline; // 解决英文字符串、数字不换行的问题 word-break: break-all; word-wrap: break-word; } } .messageItemRight { display: flex; align-items: flex-start; justify-content: flex-end; .messageContent { max-width: calc(750rpx - 10px - 50px - 15px - 10px - 50px - 15px); padding: 10px; // margin-top: 10px; border-radius: 7px; font-family: sans-serif; // padding: 10px; // 让view只包裹文字 width: auto; // display: inline-block !important; // display: inline; // 解决长英文字符串、数字不换行的问题 word-wrap: break-word; } } .right { background-color: #94EA68; } .left { background-color: #ffffff; } } .messageSend { display: flex; background: #ffffff; padding-top: 5px; padding-bottom: 15px; .messageInput { border: 1px #EBEDF0 solid; border-radius: 5px; width: calc(750rpx - 65px); margin-left: 5px; } .commmitButton { height: 38px; border-radius: 5px; width: 50px; display: flex; align-items: center; justify-content: center; color: #ffffff; background: #3C9CFF; } } </style>
这个问题属于是整串英文被以为是一个单词了,所以没有换行,看下面的句子,英文单词可以比较短的,所以会自动换行
解决这个问题只需要添加下面的css即可
// 解决长英文字符串、数字不换行的问题
word-wrap: break-word;
下面是添加之后的效果
在聊天的时候,无论是发送一条新的消息,还是接收到一条新的消息,聊天区域都需要自动滑动到最新的消息那里。本文使用scroll-view组件来包裹显示聊天消息,在scroll-view组件中,可以通过给scroll-into-view属性赋值来指定聊天区域所显示到的位置。使用时需要注意如下问题:
:style="{height:scrollViewHeight}"
,因为手机端使用小程序打字时键盘呼出会影响聊天区域的高度后续通过给scrollToView设置不同的值即可控制聊天区域的滑动,比如每接收到一条新的消息,就调用toBottom
方法,该方法通过设置scrollToView为'message' + this.messageList[this.messageList.length - 1].id
将聊天区域滑动到最新的消息处。需要注意的是,在进行该值的设置之前,需要延迟一段时间,否则滑动可能不成功,本文延迟150ms,读者也可以探索不同的值,该值不能太大或者太小。
通过设置scroll-view的属性scroll-with-animation的值为true,可以让消息区域在滑动的时候使用动画过渡,这样滑动更加自然。
当键盘呼出时,需要将聊天区域的高度减去键盘的高度。同时将scrollToView赋值为最后一条消息的id。需要注意的是,在设置scrollToView之前,需要先将scrollToView设置为空字符串,否则滑动效果可能不成功
onReady() {
// 监听键盘高度变化,以便设置输入框的高度
uni.onKeyboardHeightChange(res => {
let keyBoardHeight = res.height;
console.log("keyBoardHeight:" + keyBoardHeight);
this.scrollViewHeight = `calc(100vh - 20px - 44px - ${keyBoardHeight}px)`;
this.scrollToView = '';
setTimeout(() => {
this.scrollToView = 'message' + this.messageList[this
.messageList.length - 1].id;
}, 150)
})
},
为了便于后端在存储聊天数据的时候辨别消息是否为已读状态。比如,在小王开始聊天之前,需要先告诉后端:“小王要开始和小明聊天了”,如果正好小明也告诉后端:“我要和小王聊天了”,那小王发出去的消息就会被设置为已读状态,因为他们两个此时此刻正在同时和对方聊天,那小王发出去的消息就默认被小明看到了,因此设置为已读状态
/** * 开始聊天 */ startChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "start" } // 告诉服务端要开始聊天了 this.$socket.send({ data: JSON.stringify(message) }); }, /** * 结束聊天 */ endChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "end" } // 告诉服务端要结束聊天了 this.$socket.send({ data: JSON.stringify(message) }); },
[chat.js]
import httpRequest from '@/utils/request' // 查询聊天数据列表 export function list(query) { return httpRequest.request({ url: '/market/chat/list', method: 'get', params: query }) } // 查询最近和自己聊天的用户 export function listChatUserVo(page) { return httpRequest.request({ url: `/market/chat/listChatUserVo?pageNum=${page.pageNum}&pageSize=${page.pageSize}`, method: 'get' }) } // 查询用户和自己最近的聊天信息 export function listChat(toUsername, page) { return httpRequest.request({ url: `/market/chat/listChat/${toUsername}?pageNum=${page.pageNum}&pageSize=${page.pageSize}`, method: 'get' }) } // 查询聊天数据详细 export function getChat(id) { return httpRequest.request({ url: '/market/chat/getInfo/' + id, method: 'get' }) } // 新增聊天数据 export function addChat(data) { return httpRequest.request({ url: '/market/chat', method: 'post', data: data }) } // 修改聊天数据 export function updateChat(data) { return httpRequest.request({ url: '/market/chat', method: 'put', data: data }) } // 删除聊天数据 export function delChat(id) { return httpRequest.request({ url: '/market/chat/' + id, method: 'delete' }) }
[user.js]
import httpRequest from '@/utils/request' // 查询用户列表 export function listUser(query) { return httpRequest.request({ url: '/system/user/list', method: 'get', params: query }) } // 新增用户 export function addUser(data) { return httpRequest.request({ url: '/system/user', method: 'post', data: data }) } // 修改用户 export function updateUser(data) { return httpRequest.request({ url: '/system/user', method: 'put', data: data }) } // 删除用户 export function delUser(userId) { return httpRequest.request({ url: '/system/user/' + userId, method: 'delete' }) } // 用户密码重置 export function resetUserPwd(userId, password) { const data = { userId, password } return httpRequest.request({ url: '/system/user/resetPwd', method: 'put', data: data }) } // 用户状态修改 export function changeUserStatus(userId, status) { const data = { userId, status } return httpRequest.request({ url: '/system/user/changeStatus', method: 'put', data: data }) } // 查询用户个人信息 export function getUserProfile() { return httpRequest.request({ url: '/system/user/profile', method: 'get' }) } // 查询用户个人信息Vo export function getUserProfileVo() { return httpRequest.request({ url: '/system/user/profile/vo', method: 'get' }) } // 修改用户个人信息 export function updateUserProfile(data) { return httpRequest.request({ url: '/system/user/profile', method: 'put', data: data }) } // 用户密码重置 export function updateUserPwd(oldPassword, newPassword) { const data = { oldPassword, newPassword } return httpRequest.request({ url: '/system/user/profile/updatePwd', method: 'put', data: data }) } // 用户头像上传 export function uploadAvatar(data) { return httpRequest.request({ url: '/system/user/profile/avatar', method: 'post', data: data }) } // 查询授权角色 export function getAuthRole(userId) { return httpRequest.request({ url: '/system/user/authRole/' + userId, method: 'get' }) } // 保存授权角色 export function updateAuthRole(data) { return httpRequest.request({ url: '/system/user/authRole', method: 'put', params: data }) } // 查询部门下拉树结构 export function deptTreeSelect() { return httpRequest.request({ url: '/system/user/deptTree', method: 'get' }) }
该项目的其他文章请查看【易售小程序项目】项目介绍、小程序页面展示与系列文章集合
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。