文章目录
- 效果显示
- WebSocket连接
- 使用全局变量
- WebSocket连接细节
- 最近和自己聊天的用户信息
- 界面效果
- 界面代码
- 最近的聊天内容太长
- 日期时间显示
- 未读消息数量显示
- 私聊界面
- 界面展示
- 代码实现
- 英文长串不换行问题
- 聊天区域自动滑动到底部
- 键盘呼出,聊天区域收缩,聊天区域滑动到底部
- 通知WebSocket服务器哪两个用户开始聊天
效果显示
WebSocket连接
使用全局变量
本小程序在用户浏览首页的时候创建WebSocket连接,并将连接获得的WebSocket对象存储到全局变量中,方便其他页面来使用WebSocket
首先在项目的main.js文件中声明全局变量socket
Vue.prototype.$socket = null
对全局变量进行赋值
Vue.prototype.$socket = this.$socket;
后续如果需要使用全局变量,直接使用this.$socket
即可
WebSocket连接细节
下面的代码中有一个headbeat方法,该方法主要用来定时给WebSocket服务器发送一个信号,告诉WebSocket服务器当前客户端还处于连接状态。当心跳停止的时候(比如客户端断网),后端服务就会将用户信息从连接中移除
/*** 创建websocket连接*/
initWebsocket() {// console.log("this.socket:" + this.$socket)if (this.$socket == null || this.$socket.readyState != 1) {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;this.initWebsocket();// 连接完成,设置为falsethis.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><viewstyle="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属性赋值来指定聊天区域所显示到的位置。使用时需要注意如下问题:
- 需要先给每一条消息设置一个id属性,id属性存储的内容不能以数字开头,因此本文在id之间拼接了一个字符串’message’
- scroll-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)})
},
通知WebSocket服务器哪两个用户开始聊天
为了便于后端在存储聊天数据的时候辨别消息是否为已读状态。比如,在小王开始聊天之前,需要先告诉后端:“小王要开始和小明聊天了”,如果正好小明也告诉后端:“我要和小王聊天了”,那小王发出去的消息就会被设置为已读状态,因为他们两个此时此刻正在同时和对方聊天,那小王发出去的消息就默认被小明看到了,因此设置为已读状态
/**
* 开始聊天
*/
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)});
},