1. 说明:连接状态,客户端ID,在线状态,连接中,当前聊天会话ID,当前聊天对象ID,总未读数,
  2. 聊天功能实现首先要保证当前用户已经登录状态
  3. 监听登录时更新会话列表
  4. 监听退出时更新会话列表
  5. 发起聊天的时候,其他人个人空间的时候核心按钮旁边有一个聊天的入口按钮点击不仅要做进入聊天页面还要更新tabbar页面(下面简称会话列表)的更新会话列表(相当于发起聊天)并且会话置顶
  6. 根据聊天记录最后一条消息更新会话列表
  7. 进入会话列表开启接收消息相关补充一点下拉刷新携带参数为回调函数用于showToast和关闭下拉刷新操作其他为null
  8. 进入会话详情页面此时采用和微信历史聊天记录一样是用的翻转180度,另外chat-item组件外需要包一层view且需要margin为auto让他剧中避免一条消息的时候处于底部,当然页面布局结束,进入详情页面也是需要跟新消息的我们打开$on的监听add数据即可,但是追加好数据之后要让其滚动到底部即用scrollTop可以巧用1px置空为0让他接收到消息始终在底部this.scrollTop == 1 ? 0 : 1,其实也好理解就是soket.uts中监听消息根据后端返回type通知是否是绑定设备(用户上线)跟新会话列表还是会话详情的消息即可
  9. 更新总未读数后端处理了,我们只需要进入会话详情(两个接口一个获取聊天记录一个查看当前会话聊天记录)之后调用已读接口拿到数据之后setTabBarBadge配置,查看接口调用后携带会话详情id(已读)通知会话列表更新,会话列表更新做过滤并拿到返回值数量unread_count
  10. 这里说一下啊像\(on和\)emit全局做了很多同步更新,我们都需要在页面或者组件销毁的时候关闭一下$off
  11. websocket如果监听失败断开连接了用户下线,连接关闭,我们需要尝试重连(心跳),下面代码socket.uts已经处理了
聊天列表
<template>
<!-- #ifdef APP -->
<scroll-view style="flex:1">
<!-- #endif -->
<view class="msg-item" hover-class="msg-item-hover" v-for="(item,index) in list" :key="index" @click="openChat(item)">
<avatar :src="item.avatar" width="100rpx" height="100rpx" style="margin-right: 20rpx;"></avatar>
<view class="msg-item-body">
<text class="msg-item-nickname">{{ item.name }}</text>
<text class="msg-item-content">{{ item.last_msg_note }}</text>
</view>
<view class="msg-item-info">
<text class="msg-item-time">{{ item.update_time }}</text>
<text class="msg-item-badge" v-if="item.unread_count > 0">{{ item.unread_count > 99 ? '99+' : item.unread_count }}</text>
</view>
</view> <!-- 暂无数据 -->
<tip v-if="!isFirstLoad && list.length == 0"></tip> <loading-more v-if="isFirstLoad || list.length > 0" :loading="loading" :isEnded="isEnded"></loading-more>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template> <script>
import { Conversation,ConversationResult,Result } from '@/common/type.uts';
import { getURL } from "@/common/request.uts"
import { getToken,loginState } from "@/store/user.uts"
import { openSocket } from "@/common/socket.uts"
export default {
data() {
return {
list: [] as Conversation[],
loading: false,
isEnded: false,
currentPage: 1,
isFirstLoad:true
}
},
onLoad() {
this.refreshData(null)
// 监听会话变化
uni.$on("onUpdateConversation",this.onUpdateConversation)
uni.$on("onUpdateNoReadCount",this.onUpdateNoReadCount)
},
onShow(){
if(this.loginState){
openSocket()
}
},
onUnload() {
uni.$off("onUpdateConversation",this.onUpdateConversation)
uni.$off("onUpdateNoReadCount",this.onUpdateNoReadCount)
},
onPullDownRefresh() {
this.refreshData(()=>{
uni.showToast({
title: '刷新成功',
icon: 'none'
});
uni.stopPullDownRefresh()
})
},
onReachBottom() {
this.loadData(null)
},
computed: {
// 登录状态
loginState(): boolean {
return loginState.value
}
},
methods: {
onUpdateNoReadCount(id:number){
let item = this.list.find((o:Conversation):boolean => o.id == id)
if(item != null){
item.unread_count = 0
}
},
// 监听会话变化
onUpdateConversation(e:Conversation | null){
// 登录或者退出触发
if(e == null){
// 已登录,直接刷新数据
if(this.loginState){
this.refreshData(null)
}
// 退出登录,清除会话列表
else {
this.list.length = 0
}
return
}
// 发起会话 或 聊天中 触发
// 查询会话是否存在
let i = this.list.findIndex((o:Conversation):boolean => {
return o.id == e.id
})
// 不存在直接刷新
if(i == -1){
this.refreshData(null)
return
}
// 存在则修改并置顶
this.list[i].avatar = e.avatar
this.list[i].name = e.name
this.list[i].last_msg_note = e.last_msg_note
this.list[i].unread_count = e.unread_count
this.list[i].update_time = e.update_time
this._toFirst(this.list,i)
},
// 数组置顶
_toFirst(arr: Conversation[], index : number) : Conversation[]{
if(index != 0){
arr.unshift(arr.splice(index,1)[0])
}
return arr;
},
openChat(item : Conversation){
uni.navigateTo({
url: `/pages/chat/chat?id=${item.id}&target_id=${item.target_id}&title=${item.name}`
});
},
refreshData(loadComplete : (() => void) | null) {
this.list.length = 0
this.currentPage = 1
this.isFirstLoad = true
this.isEnded = false
this.loading = false
this.loadData(loadComplete)
},
loadData(loadComplete : (() => void) | null) {
if (this.loading || this.isEnded) {
return
}
this.loading = true
uni.request<Result<ConversationResult>>({
url: getURL(`/im/conversation/${Math.floor(this.currentPage)}`),
header:{
token:getToken()
},
success: (res) => {
let r = res.data
if(r == null) return
if(res.statusCode !=200){
uni.showToast({
title: r.msg,
icon: 'none'
});
return
} const resData = r.data as ConversationResult | null
if(resData == null) return // 是否还有数据
this.isEnded = resData.last_page <= resData.current_page
if(this.currentPage == 1){
this.list = resData.data
} else {
this.list.push(...resData.data)
} // 页码+1
this.currentPage = this.isEnded ? resData.current_page : Math.floor(resData.current_page + 1)
},
fail: (err) => {
uni.showToast({
title: err.errMsg,
icon: 'none'
});
},
complete: () => {
this.loading = false
this.isFirstLoad = false
if (loadComplete != null) {
loadComplete()
}
}
})
},
}
}
</script> <style>
.msg-item {
flex-direction: row;
align-items: stretch;
padding: 20rpx 30rpx;
}
.msg-item-hover {
background-color: #f4f4f4;
}
.msg-item-body {
max-width: 420rpx;
}
.msg-item-nickname {
font-size: 17px;
font-weight: bold;
margin: 10rpx 0;
lines: 1;
}
.msg-item-content {
font-size: 14px;
color: #727272;
lines: 1;
}
.msg-item-info {
margin-left: auto;
align-items: flex-end;
flex-shrink: 0;
}
.msg-item-time {
font-size: 12px;
color: #777777;
margin: 10rpx 0;
}
.msg-item-badge {
color: #ffffff;
background-color: #f84c2f;
font-size: 11px;
padding: 4rpx 8rpx;
border-radius: 30rpx;
font-weight: bold;
}
</style>
聊天详情
<template>
<scroll-view :scroll-top="scrollTop" class="chat-scroller" :scroll-with-animation="true" @scrolltolower="loadData(null)">
<view style="margin-top: auto;">
<chat-item v-for="(item,index) in list" :key="index" :item="item"></chat-item>
<view class="loadMore" v-if="list.length > 5">
<loading-more :isEnded="isEnded" :loading="loading"></loading-more>
</view>
</view>
</scroll-view>
<view class="chat-action">
<textarea :auto-focus="false" class="chat-input" :auto-height="true" v-model="content" placeholder="说几句吧" />
<main-btn width="100rpx" height="60rpx" font-size="14px" :disabled="content == '' || sendLoading"
style="margin-left: 10rpx;margin-bottom: 5rpx;" @click="send">{{ sendLoading ? '发送中' : '发送' }}</main-btn>
</view>
</template> <script>
import { ChatItem,ChatItemResult,Result,Conversation } from "@/common/type.uts"
import { getURL } from "@/common/request.uts"
import { getToken } from "@/store/user.uts"
import { setCurrentConversation } from "@/common/socket.uts"
export default {
data() {
return {
content: "",
list: [] as ChatItem[],
isEnded: false,
loading: false,
currentPage: 1,
sendLoading: false,
scrollTop:0,
id:0,
target_id:0
}
},
onLoad(options:OnLoadOptions) {
// 会话ID
if(options.has("id")){
this.id = parseInt(options.get("id") as string)
}
// 聊天对象ID
if(options.has("target_id")){
this.target_id = parseInt(options.get("target_id") as string)
}
// 页面标题
if(options.has("title")){
const title = options.get("title") as string
uni.setNavigationBarTitle({
title
})
}
// 设置当前聊天对象
setCurrentConversation(this.id, this.target_id)
// 获取聊天记录
this.refreshData(null)
// 监听接收信息
uni.$on("onMessage",this.onMessage)
// 更新未读数
this.read()
},
onUnload() {
// 删除当前聊天对象
setCurrentConversation(0, 0)
uni.$off("onMessage",this.onMessage)
},
methods: {
// 接收消息
onMessage(e:ChatItem){
console.log("onMessage",e)
// 属于当前会话,直接添加数据
if(e.conversation_id == this.id){
// 将数据渲染到页面
this.addMessage(e)
// 更新未读数
this.read()
}
},
// 更新未读数
read(){
uni.request<Result<Conversation>>({
url: getURL(`/im/read_conversation/${this.id}`),
method: 'POST',
header:{
token:getToken()
},
success: res => {
let r = res.data
if(r == null) return
if(res.statusCode != 200){
uni.showToast({
title: r.msg,
icon: 'none'
});
return
}
const resData = r.data as Conversation | null
if(resData == null) return
// 通知聊天会话列表更新未读数
uni.$emit("onUpdateNoReadCount",resData.id)
},
fail: (err) => {
uni.showToast({
title: err.errMsg,
icon: 'none'
});
},
});
},
refreshData(loadComplete : (() => void) | null) {
this.list.length = 0
this.currentPage = 1
this.isEnded = false
this.loading = false
this.loadData(loadComplete)
},
loadData(loadComplete : (() => void) | null) {
if (this.loading || this.isEnded) {
return
}
this.loading = true
uni.request<Result<ChatItemResult>>({
url: getURL(`/im/${this.id}/message/${Math.floor(this.currentPage)}`),
header:{
token:getToken()
},
success: (res) => {
let r = res.data
if(r == null) return
if(res.statusCode !=200){
uni.showToast({
title: r.msg,
icon: 'none'
});
return
} const resData = r.data as ChatItemResult | null
if(resData == null) return // 是否还有数据
this.isEnded = resData.last_page <= resData.current_page
if(this.currentPage == 1){
this.list = resData.data
} else {
this.list.push(...resData.data)
} // 页码+1
this.currentPage = this.isEnded ? resData.current_page : Math.floor(resData.current_page + 1)
},
fail: (err) => {
uni.showToast({
title: err.errMsg,
icon: 'none'
});
},
complete: () => {
this.loading = false
if (loadComplete != null) {
loadComplete()
}
}
})
},
send() {
this.sendLoading = true uni.request<Result<ChatItem>>({
url:getURL("/im/send"),
method:"POST",
header:{
token:getToken()
},
data: {
target_id:this.target_id,
type:"text",
body:this.content,
client_create_time: Date.now()
},
success:(res)=>{
let r = res.data
if(r == null) return
if(res.statusCode != 200){
uni.showToast({
title: r.msg,
icon: 'none'
});
return
}
if(r.data == null) return
let d = r.data as ChatItem
/**
* 消息状态state:
* 100 发送成功
* 101 对方已把你拉黑
* 102 你把对方拉黑了
* 103 对方已被系统封禁
* 104 禁止发送(内容不合法)
*/
if(d.state != 100){
let title = d.state_text != null ? d.state_text as string : '发送失败'
uni.showToast({
title,
icon: 'none'
});
} this.addMessage(d)
this.content = ""
},
fail:(err)=>{
uni.showToast({
title: err.errMsg,
icon: 'none'
});
},
complete:()=>{
this.sendLoading = false
}
})
},
// 添加数据
addMessage(e:ChatItem){
// 将最新的数据追加到列表头部
this.list.unshift(e)
this.goToBottom()
},
// 滚动到底部
goToBottom(){
setTimeout(()=>{
this.scrollTop = this.scrollTop == 1 ? 0 : 1
},300)
}
}
}
</script> <style>
.chat-scroller {
flex: 1;
box-sizing: border-box;
transform: rotate(180deg);
} .loadMore {
transform: rotate(180deg);
} .chat-action {
min-height: 95rpx;
flex-direction: row;
align-items: flex-end;
background-color: #ffffff;
border-top: 1px solid #eeeeee;
padding-left: 28rpx;
padding-right: 28rpx;
padding-bottom: 20rpx;
flex-shrink: 0;
} .chat-input {
width: 590rpx;
background-color: #f4f4f4;
border-radius: 5px;
padding: 16rpx 20rpx;
margin-top: 20rpx;
max-height: 500rpx;
}
</style>
socket.uts
import { websocketURL } from "@/common/config.uts"
import { defaultResult,ChatItem,Conversation } from '@/common/type.uts';
import { getURL } from '@/common/request.uts';
import { getToken } from '@/store/user.uts';
// 连接状态
export const isConnect = ref<boolean>(false)
// 客户端ID
const client_id = ref<string>("")
// 在线状态
export const isOnline = ref<boolean>(false)
// 连接中
export const onlining = ref<boolean>(false)
// 当前聊天会话ID
export const current_conversation_id = ref<number>(0)
// 当前聊天对象ID
export const current_target_id = ref<number>(0)
// 总未读数
export const total_unread_count = ref<number>(0) // 设置当前会话信息
export function setCurrentConversation(conversation_id : number, target_id : number){
current_conversation_id.value = conversation_id
current_target_id.value = target_id } // 打开websocket
export function openSocket(){
// 绑定上线(防止用户处于离线状态)
handleBindOnline()
// 已连接,直接返回
if(isConnect.value) return
uni.connectSocket({
url:websocketURL
})
// 监听打开
uni.onSocketOpen((_)=>{
console.log("已连接")
isConnect.value = true
// 重置重连次数
resetReconnectAttempts()
})
// 监听关闭
uni.onSocketClose((res:OnSocketCloseCallbackResult)=>{
// 已断开
isConnect.value = false
client_id.value = ""
isOnline.value = false
if(res.code == 1000){
console.log("websocket已干净关闭,未尝试重新连接")
} else {
console.log("websocket意外断开,正在尝试重新连接")
reconnect()
}
})
// 监听失败
uni.onSocketError((res:OnSocketErrorCallbackResult)=>{
// 已断开
isConnect.value = false
client_id.value = ""
isOnline.value = false
console.log("失败 socket")
console.log(res)
}) // 监听接收消息
uni.onSocketMessage((res:OnSocketMessageCallbackResult)=>{
console.log("消息 socket")
let d = JSON.parse(res.data as string) as UTSJSONObject
const type = d.get("type") as string
switch (type){
case "bind": // 绑定上线
client_id.value = d.get("data") as string
handleBindOnline()
break;
case "message": // 接收消息
let data2 = JSON.parse<ChatItem>(JSON.stringify(d.get("data")))
uni.$emit("onMessage",data2)
break;
case "conversation": // 更新会话列表
let data1 = JSON.parse<Conversation>(JSON.stringify(d.get("data")))
uni.$emit('onUpdateConversation',data1)
break;
case "total_unread_count": // 总未读数更新
total_unread_count.value = d.get("data") as number
let total = total_unread_count.value
if(total > 0){
uni.setTabBarBadge({
index:2,
text:total > 99 ? "99+" : total.toString()
})
} else {
uni.removeTabBarBadge({
index: 2
})
}
break;
}
})
} // 关闭socket
export function closeSocket(){
uni.closeSocket({ code:1000 })
} // 绑定上线
export function handleBindOnline(){
if(isConnect.value && client_id.value != '' && !isOnline.value && !onlining.value){
onlining.value = true
const cid = client_id.value as string
uni.request<defaultResult>({
url: getURL("/im/bind_online"),
method: 'POST',
header: {
token:getToken()
},
data: {
client_id:cid
},
success: res => {
let r = res.data
if(r == null) return
// 请求失败
if(res.statusCode != 200){
uni.showToast({
title: r.msg,
icon: 'none'
});
return
}
isOnline.value = true
console.log("用户上线")
},
fail: (err) => {
uni.showToast({
title: err.errMsg,
icon: 'none'
});
},
complete: () => {
onlining.value = false
}
});
}
} // 已经重连次数
let reconnectAttemptCount = ref<number>(0)
// 最大自动重连数
let reconnectAttempts = 5
// 重连倒计时定时器
let reconnectInterval = 0 function reconnect():void {
console.log("重连中...")
// 如果没有超过最大重连数,继续
if(reconnectAttemptCount.value < reconnectAttempts){
// 重连次数+1
reconnectAttemptCount.value++
// 延迟重连
reconnectInterval = setTimeout(()=>{
openSocket()
}, getReconnectDelay(reconnectAttemptCount.value))
} else {
console.log("已经达到最大重连尝试次数")
}
} // 获取重连倒计时
function getReconnectDelay(attempt:number) : number {
// 最小延迟时间(毫秒)
const baseDelay = 1000;
// 最大延迟时间(毫秒)
const maxDelay = 10000;
// 根据已经重连次数,计算出本次重连倒计时
const delay = baseDelay * (2 * attempt) + Math.random() * 1000
// 取最小值
return Math.min(delay,maxDelay)
} // 重置重连次数
function resetReconnectAttempts():void {
if(reconnectInterval > 0){
clearInterval(reconnectInterval)
reconnectInterval = 0
}
reconnectAttemptCount.value = 0
}

聊天chat封装的更多相关文章

  1. 局域网聊天Chat(马士兵视频改进版)

    Github地址: https://github.com/BenDanChen/Chat Chat 小小的聊天系统,主要是跟着网上的马士兵老师的公开视频然后再自己反思有什么地方需要改进的地方,然后大体 ...

  2. 重构 JAVA 聊天室 —— CS 模式的简单架构实现

    前言 自从开始弄起数据挖掘之后,已经很久没写过技术类的博客了,最近学校 JAVA 课设要求实现一个聊天室,想想去年自己已经写了一个了,但是有些要求到的功能我也没实现,但看着原有的代码想了想加功能好像有 ...

  3. IOS要用到的东西

    code4app.com 这网站不错,收集各种 iOS App 开发可以用到的代码示例 cocoacontrols.com/ 英文版本的lib收集 objclibs.com/ 精品lib的收集网站 h ...

  4. OS开发(Objective-C)常用库索引

    code4app.com 这网站不错,收集各种 iOS App 开发可以用到的代码示例 cocoacontrols.com/ 英文版本的lib收集 objclibs.com/ 精品lib的收集网站 h ...

  5. 大型JavaScript应用程序架构模式

    11月中旬在伦敦举行的jQuery Summit顶级大会上有个session讲的是大型JavaScript应用程序架构,看完PPT以后觉得甚是不错,于是整理一下发给大家共勉. PDF版的PPT下载地址 ...

  6. iOS开发(Objective-C)常用库索引

    code4app.com 这网站不错,收集各种 iOS App 开发可以用到的代码示例 cocoacontrols.com/ 英文版本的lib收集 objclibs.com/ 精品lib的收集网站 h ...

  7. twisted(3)--再谈twisted

    上一章,我们直接写了一个小例子来从整体讲述twisted运行的大致过程,今天我们首先深入一些概念,在逐渐明白这些概念以后,我们会修改昨天写的例子. 先看下面一张图: 这个系列的第一篇文章,我们已经为大 ...

  8. iOS各种类

    http://www.isenhao.com/xueke/jisuanji/bcyy/objc.php http://www.code4app.com 这网站不错,收集各种 iOS App 开发可以用 ...

  9. [Android Pro] 终极组件化框架项目方案详解

    cp from : https://blog.csdn.net/pochenpiji159/article/details/78660844 前言 本文所讲的组件化案例是基于自己开源的组件化框架项目g ...

  10. Android组件化框架项目详解

    简介 什么是组件化? 项目发展到一定阶段时,随着需求的增加以及频繁地变更,项目会越来越大,代码变得越来越臃肿,耦合会越来越多,开发效率也会降低,这个时候我们就需要对旧项目进行重构即模块的拆分,官方的说 ...

随机推荐

  1. apisix~升级原始插件的方法

    扩展apisix原始插件 当apisix提供的插件不能满足我们要求时,我们可能需要将它的plugin进行个性化扩展,例如一个jwt认证插件jwt-auth,它本身具有验证jwt有效性功能,支持rs25 ...

  2. 新一代AI搜索引擎神器推荐及效果测试:秘塔AI、天工AI、Perplexity等

    新一代AI搜索引擎神器推荐效果测试:秘塔AI.天工AI.Perplexity等 0.前言: 搜索的核心:事物对象级别的搜索 回到搜索引擎本身,搜索引擎的早期出现是为了解决互联网上信息过载的问题.随着互 ...

  3. Instsrv.exe 与 Srvany.exe 安装Windows服务

    原理:Instsrv.exe可以给系统安装和删除服务 Srvany.exe可以让exe程序以服务的方式运行(Srvany只是exe注册程序的服务外壳,可以通过它让我们的程序以SYSTEM账户活动,随电 ...

  4. MyBatis延迟加载策略详解

    延迟加载就是在需要用到数据的时候才进行加载,不需要用到数据的时候就不加载数据.延迟加载也称为懒加载. 优点:在使用关联对象时,才从数据库中查询关联数据,大大降低数据库不必要开销. 缺点:因为只有当需要 ...

  5. Hangfire 使用笔记 任务可以分离到别的项目中,无需重复部署Hangfire,通过API方式通信。

    "巨人们"的地址 Hangfire Mysql: https://github.com/arnoldasgudas/Hangfire.MySqlStorage 在获取set表数据的 ...

  6. 算法金 | 再见,支持向量机 SVM!

    大侠幸会,在下全网同名「算法金」 0 基础转 AI 上岸,多个算法赛 Top 「日更万日,让更多人享受智能乐趣」 今日 170+/10000 一.SVM概述 定义与基本概念 支持向量机(SVM)是一种 ...

  7. 面试必会->Redis篇

    01- 你们项目中哪里用到了Redis ? 在我们的项目中很多地方都用到了Redis , Redis在我们的项目中主要有三个作用 : 使用Redis做热点数据缓存/接口数据缓存 使用Redis存储一些 ...

  8. testArticle

    Test Article This is a test article for ArticleSync. Test Edit...... test Edit

  9. 从零开始的常用MySQL语句练习大全

    先说一些废话 很多时候深入学习固然很重要,但是想要写下一篇给新手都能看得懂看的很香,并且老鸟可以查漏补缺的的练习博客,还是挺有难度, 所以今天尝试写一些关于MySQL的语句练习大全,供想要从零开始练习 ...

  10. macbookrpro使用体验

    前言 之前用的电脑是拯救者y7000 2020,用了四五年,年前就有换电脑的打算.计划就是买一个苹果电脑,在查看了挺多电脑,多方面对比后,最终还是买了Macbook pro. 我买的笔记本的配置如下: ...