文章目录
- 效果显示
- 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();
// 连接完成,设置为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属性赋值来指定聊天区域所显示到的位置。使用时需要注意如下问题:
- 需要先给每一条消息设置一个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)
});
},