ws更新
Some checks failed
Check / lint (push) Has been cancelled
Check / typecheck (push) Has been cancelled
Check / build (build, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build, 18.x, windows-latest) (push) Has been cancelled
Check / build (build:app, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build:app, 18.x, windows-latest) (push) Has been cancelled
Check / build (build:mp-weixin, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build:mp-weixin, 18.x, windows-latest) (push) Has been cancelled
Some checks failed
Check / lint (push) Has been cancelled
Check / typecheck (push) Has been cancelled
Check / build (build, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build, 18.x, windows-latest) (push) Has been cancelled
Check / build (build:app, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build:app, 18.x, windows-latest) (push) Has been cancelled
Check / build (build:mp-weixin, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build:mp-weixin, 18.x, windows-latest) (push) Has been cancelled
This commit is contained in:
parent
aab593f281
commit
fd060743bf
5
env/.env.test
vendored
5
env/.env.test
vendored
@ -5,4 +5,7 @@ VITE_SHOW_CONSOLE = true
|
||||
# 是否开启sourcemap
|
||||
VITE_SHOW_SOURCEMAP = true
|
||||
# baseUrl
|
||||
VITE_BASEURL = 'https://warehouse.szjixun.cn/oa_backend'
|
||||
# VITE_BASEURL = 'https://warehouse.szjixun.cn/oa_backend'
|
||||
VITE_BASEURL = 'http://192.168.88.59:9503'
|
||||
#VITE_SOCKET_API
|
||||
VITE_SOCKET_API = 'ws://192.168.88.59:9504'
|
||||
|
@ -1,8 +1,13 @@
|
||||
<script setup>
|
||||
import {useStatus} from "@/store/status";
|
||||
import ws from '@/connect'
|
||||
const {statusBarHeight}= useStatus()
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--statusBarHeight',`${statusBarHeight.value}px`)
|
||||
const init = () => {
|
||||
ws.connect()
|
||||
}
|
||||
init()
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@/static/css/color.scss";
|
||||
|
160
src/api/chat/index.js
Normal file
160
src/api/chat/index.js
Normal file
@ -0,0 +1,160 @@
|
||||
import request from '@/service/index.js'
|
||||
|
||||
// 获取聊天列表服务接口
|
||||
export const ServeGetTalkList = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 聊天列表创建服务接口
|
||||
export const ServeCreateTalkList = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/create',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 删除聊天列表服务接口
|
||||
export const ServeDeleteTalkList = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/delete',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 对话列表置顶服务接口
|
||||
export const ServeTopTalkList = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/topping',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 清除聊天消息未读数服务接口
|
||||
export const ServeClearTalkUnreadNum = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/unread/clear',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取聊天记录服务接口
|
||||
export const ServeTalkRecords = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/records',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取转发会话记录详情列表服务接口
|
||||
export const ServeGetForwardRecords = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/records/forward',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 对话列表置顶服务接口
|
||||
export const ServeSetNotDisturb = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/disturb',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 查找用户聊天记录服务接口
|
||||
export const ServeFindTalkRecords = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/records/history',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索用户聊天记录服务接口
|
||||
export const ServeSearchTalkRecords = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/search-chat-records',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeGetRecordsContext = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/get-records-context',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 发送代码块消息服务接口
|
||||
export const ServePublishMessage = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/message/publish',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 发送聊天文件服务接口
|
||||
export const ServeSendTalkFile = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/message/file',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 撤回消息服务接口
|
||||
export const ServeRevokeRecords = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/message/revoke',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 删除消息服务接口
|
||||
export const ServeRemoveRecords = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/message/delete',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 收藏表情包服务接口
|
||||
export const ServeCollectEmoticon = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/message/collect',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeSendVote = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/message/vote',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeConfirmVoteHandle = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/talk/message/vote/handle',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
55
src/api/emoticon/index.js
Normal file
55
src/api/emoticon/index.js
Normal file
@ -0,0 +1,55 @@
|
||||
import request from '@/service/index.js'
|
||||
|
||||
// 查询用户表情包服务接口
|
||||
export const ServeFindUserEmoticon = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/emoticon/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 查询系统表情包服务接口
|
||||
export const ServeFindSysEmoticon = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/emoticon/system/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置用户表情包服务接口
|
||||
export const ServeSetUserEmoticon = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/emoticon/system/install',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 移除收藏表情包服务接口
|
||||
export const ServeDelCollectEmoticon = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/emoticon/del-collect-emoticon',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 上传表情包服务接口
|
||||
export const ServeUploadEmoticon = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/emoticon/customize/create',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeDeleteEmoticon = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/emoticon/customize/delete',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
217
src/api/group/index.js
Normal file
217
src/api/group/index.js
Normal file
@ -0,0 +1,217 @@
|
||||
import request from '@/service/index.js'
|
||||
|
||||
// 查询用户群聊服务接口
|
||||
export const ServeGetGroups = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeGroupOvertList = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/overt/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取群信息服务接口
|
||||
export const ServeGroupDetail = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/detail',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建群聊服务接口
|
||||
export const ServeCreateGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/create',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 修改群信息
|
||||
export const ServeEditGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/setting',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 邀请好友加入群聊服务接口
|
||||
export const ServeInviteGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/invite',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 移除群聊成员服务接口
|
||||
export const ServeRemoveMembersGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/member/remove',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 管理员解散群聊服务接口
|
||||
export const ServeDismissGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/dismiss',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeMuteGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/mute',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeOvertGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/overt',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 用户退出群聊服务接口
|
||||
export const ServeSecedeGroup = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/secede',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 修改群聊名片服务接口
|
||||
export const ServeUpdateGroupCard = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/member/remark',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户可邀请加入群聊的好友列表
|
||||
export const ServeGetInviteFriends = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/member/invites',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取群聊成员列表
|
||||
export const ServeGetGroupMembers = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/member/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取群聊公告列表
|
||||
export const ServeGetGroupNotices = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/notice/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑群公告
|
||||
export const ServeEditGroupNotice = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/notice/edit',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeGetGroupApplyList = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/apply/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeGetGroupApplyAll = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/apply/all',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeDeleteGroupApply = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/apply/decline',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeAgreeGroupApply = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/apply/agree',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeCreateGroupApply = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/apply/create',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeGroupApplyUnread = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/apply/unread',
|
||||
method: 'GET',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 转让群主
|
||||
export const ServeGroupHandover = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/handover',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 分配管理员
|
||||
export const ServeGroupAssignAdmin = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/assign-admin',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const ServeGroupNoSpeak = (data) => {
|
||||
return request({
|
||||
url: '/api/v1/group/no-speak',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
188
src/connect.js
Normal file
188
src/connect.js
Normal file
@ -0,0 +1,188 @@
|
||||
// import { h } from 'vue'
|
||||
// import { NAvatar } from 'naive-ui'
|
||||
import { useTalkStore,useUserStore } from '@/store'
|
||||
// import { notifyIcon } from '@/constant/default'
|
||||
import WsSocket from './plugins/ws-socket'
|
||||
import EventTalk from './event/talk'
|
||||
// import EventKeyboard from './event/keyboard'
|
||||
// import EventLogin from './event/login'
|
||||
import EventRevoke from './event/revoke'
|
||||
// import { getAccessToken, isLoggedIn } from './utils/auth'
|
||||
import { useAuth } from "@/store/auth";
|
||||
|
||||
const { token } = useAuth()
|
||||
|
||||
const urlCallback = () => {
|
||||
// if (!isLoggedIn()) {
|
||||
// window.location.reload()
|
||||
// }
|
||||
|
||||
return `${import.meta.env.VITE_SOCKET_API}/wss/default.io?token=${token.value}`
|
||||
}
|
||||
|
||||
class Connect {
|
||||
conn
|
||||
|
||||
constructor() {
|
||||
this.conn = new WsSocket(urlCallback, {
|
||||
onError: (evt) => {
|
||||
console.log('Websocket 连接失败回调方法', evt)
|
||||
},
|
||||
// Websocket 连接成功回调方法
|
||||
onOpen: () => {
|
||||
// 更新 WebSocket 连接状态
|
||||
useUserStore().updateSocketStatus(true)
|
||||
// online.value = true;
|
||||
useTalkStore().loadTalkList()
|
||||
},
|
||||
// Websocket 断开连接回调方法
|
||||
onClose: () => {
|
||||
// 更新 WebSocket 连接状态
|
||||
useUserStore().updateSocketStatus(false)
|
||||
// online.value = false
|
||||
}
|
||||
})
|
||||
|
||||
this.bindEvents()
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接
|
||||
*/
|
||||
connect() {
|
||||
this.conn.connection()
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect() {
|
||||
this.conn.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态
|
||||
* @returns WebSocket 连接状态
|
||||
*/
|
||||
isConnect() {
|
||||
if (!this.conn.connect) return false
|
||||
|
||||
return this.conn.connect.readyState === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送事件消息
|
||||
* @param event 事件名
|
||||
* @param data 数据
|
||||
*/
|
||||
emit(event, data) {
|
||||
this.conn.emit(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定监听消息事件
|
||||
*/
|
||||
bindEvents() {
|
||||
this.onPing()
|
||||
this.onPong()
|
||||
this.onImMessage()
|
||||
// this.onImMessageRead()
|
||||
// this.onImContactStatus()
|
||||
this.onImMessageRevoke()
|
||||
// this.onImMessageKeyboard()
|
||||
}
|
||||
|
||||
onPing() {
|
||||
this.conn.on('ping', () => this.emit('pong', ''))
|
||||
}
|
||||
|
||||
onPong() {
|
||||
this.conn.on('pong', () => {})
|
||||
}
|
||||
|
||||
onImMessage() {
|
||||
this.conn.on('im.message', (data) => new EventTalk(data))
|
||||
}
|
||||
|
||||
// onImMessageRead() {
|
||||
// this.conn.on('im.message.read', (data) => {
|
||||
// const dialogueStore = useDialogueStore()
|
||||
|
||||
// if (dialogueStore.index_name !== `1_${data.sender_id}`) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const { msg_ids = [] } = data
|
||||
|
||||
// for (const msgid of msg_ids) {
|
||||
// dialogueStore.updateDialogueRecord({ msg_id: msgid, is_read: 1 })
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
onImContactStatus() {
|
||||
// 好友在线状态事件
|
||||
// this.conn.on('im.contact.status', (data) => new EventLogin(data))
|
||||
}
|
||||
|
||||
onImMessageKeyboard() {
|
||||
// 好友键盘输入事件
|
||||
// this.conn.on('im.message.keyboard', (data) => new EventKeyboard(data))
|
||||
}
|
||||
|
||||
// 即将废弃
|
||||
onImMessageRevoke() {
|
||||
// 消息撤回事件
|
||||
this.conn.on('im.message.revoke', (data) => new EventRevoke(data))
|
||||
}
|
||||
|
||||
onImContactApply() {
|
||||
// 好友申请事件
|
||||
// this.conn.on('im.contact.apply', (data) => {
|
||||
// window['$notification'].create({
|
||||
// title: '好友申请通知',
|
||||
// content: data.remark,
|
||||
// description: `申请人: ${data.friend.nickname}`,
|
||||
// meta: data.friend.created_at,
|
||||
// avatar: () =>
|
||||
// h(NAvatar, {
|
||||
// size: 'small',
|
||||
// round: true,
|
||||
// src: notifyIcon,
|
||||
// style: 'background-color:#fff;'
|
||||
// }),
|
||||
// duration: 3000
|
||||
// })
|
||||
// useUserStore().isContactApply = true
|
||||
// })
|
||||
}
|
||||
|
||||
onImGroupApply() {
|
||||
// 群申请消息
|
||||
// this.conn.on('im.group.apply', () => {
|
||||
// window['$notification'].create({
|
||||
// title: '入群申请通知',
|
||||
// content: '有新的入群申请,请注意查收',
|
||||
// avatar: () =>
|
||||
// h(NAvatar, {
|
||||
// size: 'small',
|
||||
// round: true,
|
||||
// src: notifyIcon,
|
||||
// style: 'background-color:#fff;'
|
||||
// }),
|
||||
// duration: 30000
|
||||
// })
|
||||
|
||||
// useUserStore().isGroupApply = true
|
||||
// })
|
||||
}
|
||||
|
||||
onEventError() {
|
||||
this.conn.on('event_error', (data) => {
|
||||
// window['$message'] && window['$message'].error(JSON.stringify(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export default new Connect()
|
96
src/constant/message.ts
Normal file
96
src/constant/message.ts
Normal file
@ -0,0 +1,96 @@
|
||||
export const ChatMsgTypeText = 1 // 文本消息
|
||||
export const ChatMsgTypeCode = 2 // 代码消息
|
||||
export const ChatMsgTypeImage = 3 // 图片文件
|
||||
export const ChatMsgTypeAudio = 4 // 语音文件
|
||||
export const ChatMsgTypeVideo = 5 // 视频文件
|
||||
export const ChatMsgTypeFile = 6 // 其它文件
|
||||
export const ChatMsgTypeLocation = 7 // 位置消息
|
||||
export const ChatMsgTypeCard = 8 // 名片消息
|
||||
export const ChatMsgTypeForward = 9 // 转发消息
|
||||
export const ChatMsgTypeLogin = 10 // 登录消息
|
||||
export const ChatMsgTypeVote = 11 // 投票消息
|
||||
export const ChatMsgTypeMixed = 12 // 混合消息
|
||||
export const ChatMsgTypeGroupNotice = 13 // 群公告消息
|
||||
|
||||
export const ChatMsgSysText = 1000 // 系统文本消息
|
||||
export const ChatMsgSysGroupCreate = 1101 // 创建群聊消息
|
||||
export const ChatMsgSysGroupMemberJoin = 1102 // 加入群聊消息
|
||||
export const ChatMsgSysGroupMemberQuit = 1103 // 群成员退出群消息
|
||||
export const ChatMsgSysGroupMemberKicked = 1104 // 踢出群成员消息
|
||||
export const ChatMsgSysGroupMessageRevoke = 1105 // 管理员撤回成员消息
|
||||
export const ChatMsgSysGroupDismissed = 1106 // 群解散
|
||||
export const ChatMsgSysGroupMuted = 1107 // 群禁言
|
||||
export const ChatMsgSysGroupCancelMuted = 1108 // 群解除禁言
|
||||
export const ChatMsgSysGroupMemberMuted = 1109 // 群成员禁言
|
||||
export const ChatMsgSysGroupMemberCancelMuted = 1110 // 群成员解除禁言
|
||||
export const ChatMsgSysGroupNotice = 1111 // 编辑群公告
|
||||
export const ChatMsgSysGroupTransfer = 1113 // 变更群主
|
||||
|
||||
export const ChatMsgTypeMapping = {
|
||||
[ChatMsgTypeText]: '[文本消息]',
|
||||
[ChatMsgTypeImage]: '[图片消息]',
|
||||
[ChatMsgTypeAudio]: '[语音消息]',
|
||||
[ChatMsgTypeVideo]: '[视频消息]',
|
||||
[ChatMsgTypeFile]: '[文件消息]',
|
||||
[ChatMsgTypeLocation]: '[位置消息]',
|
||||
[ChatMsgTypeCard]: '[名片消息]',
|
||||
[ChatMsgTypeForward]: '[转发消息]',
|
||||
[ChatMsgTypeLogin]: '[登录消息]',
|
||||
[ChatMsgTypeVote]: '[投票消息]',
|
||||
[ChatMsgTypeCode]: '[代码消息]',
|
||||
[ChatMsgTypeMixed]: '[图文消息]',
|
||||
[ChatMsgTypeGroupNotice]: '[群公告]',
|
||||
[ChatMsgSysText]: '[系统消息]',
|
||||
[ChatMsgSysGroupCreate]: '[创建群消息]',
|
||||
[ChatMsgSysGroupMemberJoin]: '[加入群消息]',
|
||||
[ChatMsgSysGroupMemberQuit]: '[退出群消息]',
|
||||
[ChatMsgSysGroupMemberKicked]: '[踢出群消息]',
|
||||
[ChatMsgSysGroupMessageRevoke]: '[撤回消息]',
|
||||
[ChatMsgSysGroupDismissed]: '[群解散消息]',
|
||||
[ChatMsgSysGroupMuted]: '[群禁言消息]',
|
||||
[ChatMsgSysGroupCancelMuted]: '[群解除禁言消息]',
|
||||
[ChatMsgSysGroupMemberMuted]: '[群成员禁言消息]',
|
||||
[ChatMsgSysGroupMemberCancelMuted]: '[群成员解除禁言消息]',
|
||||
[ChatMsgSysGroupNotice]: '[群公告]'
|
||||
}
|
||||
|
||||
// 消息类型 - 消息组件 映射关系
|
||||
export const MessageComponents = {
|
||||
[ChatMsgTypeText]: 'text-message',
|
||||
[ChatMsgTypeImage]: 'image-message',
|
||||
[ChatMsgTypeAudio]: 'audio-message',
|
||||
[ChatMsgTypeVideo]: 'video-message',
|
||||
[ChatMsgTypeFile]: 'file-message',
|
||||
[ChatMsgTypeLocation]: 'location-message',
|
||||
[ChatMsgTypeCard]: 'user-card-message',
|
||||
[ChatMsgTypeForward]: 'forward-message',
|
||||
[ChatMsgTypeLogin]: 'login-message',
|
||||
[ChatMsgTypeVote]: 'vote-message',
|
||||
[ChatMsgTypeCode]: 'code-message',
|
||||
[ChatMsgTypeMixed]: 'mixed-message',
|
||||
[ChatMsgTypeGroupNotice]: 'group-notice-message',
|
||||
[ChatMsgSysText]: 'sys-text-message',
|
||||
[ChatMsgSysGroupCreate]: 'sys-group-create-message',
|
||||
[ChatMsgSysGroupMemberJoin]: 'sys-group-join-message',
|
||||
[ChatMsgSysGroupMemberQuit]: 'sys-group-member-quit-message',
|
||||
[ChatMsgSysGroupMemberKicked]: 'sys-group-member-kicked-message',
|
||||
// [ChatMsgSysGroupMessageRevoke]: '[撤回消息]',
|
||||
// [ChatMsgSysGroupDismissed]: '[群解散消息]',
|
||||
[ChatMsgSysGroupMuted]: 'sys-group-muted-message',
|
||||
[ChatMsgSysGroupCancelMuted]: 'sys-group-cancel-muted-message',
|
||||
[ChatMsgSysGroupMemberMuted]: 'sys-group-member-muted-message',
|
||||
[ChatMsgSysGroupMemberCancelMuted]: 'sys-group-member-cancel-muted-message',
|
||||
[ChatMsgSysGroupTransfer]: 'sys-group-transfer-message'
|
||||
}
|
||||
|
||||
// 可转发的消息类型
|
||||
export const ForwardableMessageType = [
|
||||
ChatMsgTypeText,
|
||||
ChatMsgTypeCode,
|
||||
ChatMsgTypeImage,
|
||||
ChatMsgTypeAudio,
|
||||
ChatMsgTypeVideo,
|
||||
ChatMsgTypeFile,
|
||||
ChatMsgTypeLocation,
|
||||
ChatMsgTypeCard
|
||||
]
|
59
src/event/base.js
Normal file
59
src/event/base.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { useDialogueStore } from '@/store'
|
||||
// import router from '@/router'
|
||||
import { useAuth } from "@/store/auth/index.js";
|
||||
|
||||
const { userInfo } = useAuth()
|
||||
|
||||
class Base {
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的ID
|
||||
*/
|
||||
getAccountId() {
|
||||
return userInfo.value.ID
|
||||
}
|
||||
|
||||
getTalkParams() {
|
||||
let dialogueStore = useDialogueStore()
|
||||
|
||||
let { talk_type, receiver_id } = dialogueStore.talk
|
||||
|
||||
return {
|
||||
talk_type,
|
||||
receiver_id,
|
||||
index_name: dialogueStore.index_name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断消息是否来自当前对话
|
||||
*
|
||||
* @param {Number} talk_type 聊天消息类型[1:私信;2:群聊;]
|
||||
* @param {Number} sender_id 发送者ID
|
||||
* @param {Number} receiver_id 接收者ID
|
||||
*/
|
||||
isTalk(talk_type, sender_id, receiver_id) {
|
||||
let params = this.getTalkParams()
|
||||
|
||||
if (talk_type != params.talk_type) {
|
||||
return false
|
||||
} else if (params.receiver_id == receiver_id || params.receiver_id == sender_id) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否打开对话页
|
||||
*/
|
||||
// isTalkPage() {
|
||||
// return ['/message', '/'].includes(router.currentRoute.value.path)
|
||||
// }
|
||||
}
|
||||
|
||||
export default Base
|
88
src/event/revoke.js
Normal file
88
src/event/revoke.js
Normal file
@ -0,0 +1,88 @@
|
||||
import Base from './base'
|
||||
import { useDialogueStore, useTalkStore } from '@/store'
|
||||
import { parseTime } from '@/utils/datetime'
|
||||
|
||||
/**
|
||||
* 好友状态事件
|
||||
*/
|
||||
class Revoke extends Base {
|
||||
/**
|
||||
* @var resource 资源
|
||||
*/
|
||||
resource
|
||||
|
||||
/**
|
||||
* 发送者ID
|
||||
*/
|
||||
sender_id = 0
|
||||
|
||||
/**
|
||||
* 接收者ID
|
||||
*/
|
||||
receiver_id = 0
|
||||
|
||||
/**
|
||||
* 聊天类型[1:私聊;2:群聊;]
|
||||
*/
|
||||
talk_type = 0
|
||||
|
||||
/**
|
||||
* 初始化构造方法
|
||||
*
|
||||
* @param {Object} resource Socket消息
|
||||
*/
|
||||
constructor(resource) {
|
||||
super()
|
||||
|
||||
this.resource = resource
|
||||
this.sender_id = resource.sender_id
|
||||
this.receiver_id = resource.receiver_id
|
||||
this.talk_type = resource.talk_type
|
||||
this.msg_id = resource.msg_id
|
||||
|
||||
this.handle()
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断消息发送者是否来自于我
|
||||
* @returns
|
||||
*/
|
||||
isCurrSender() {
|
||||
return this.sender_id == this.getAccountId()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话索引
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
getIndexName() {
|
||||
if (this.talk_type == 2) {
|
||||
return `${this.talk_type}_${this.receiver_id}`
|
||||
}
|
||||
|
||||
let receiver_id = this.isCurrSender() ? this.receiver_id : this.sender_id
|
||||
|
||||
return `${this.talk_type}_${receiver_id}`
|
||||
}
|
||||
|
||||
handle() {
|
||||
useTalkStore().updateItem({
|
||||
index_name: this.getIndexName(),
|
||||
msg_text: this.resource.text,
|
||||
updated_at: parseTime(new Date())
|
||||
})
|
||||
|
||||
// 判断当前是否正在和好友对话
|
||||
if (!this.isTalk(this.talk_type, this.receiver_id, this.sender_id)) {
|
||||
return
|
||||
}
|
||||
|
||||
useDialogueStore().updateDialogueRecord({
|
||||
msg_id: this.msg_id,
|
||||
is_revoke: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Revoke
|
228
src/event/talk.js
Normal file
228
src/event/talk.js
Normal file
@ -0,0 +1,228 @@
|
||||
import Base from './base'
|
||||
import { nextTick } from 'vue'
|
||||
import ws from '@/connect'
|
||||
import { parseTime } from '@/utils/datetime'
|
||||
import * as message from '@/constant/message'
|
||||
import { formatTalkItem, palyMusic, formatTalkRecord } from '@/utils/talk'
|
||||
// import { isElectronMode } from '@/utils/common'
|
||||
import { ServeClearTalkUnreadNum, ServeCreateTalkList } from '@/api/chat/index.js'
|
||||
import { useTalkStore, useDialogueStore } from '@/store'
|
||||
|
||||
/**
|
||||
* 好友状态事件
|
||||
*/
|
||||
class Talk extends Base {
|
||||
/**
|
||||
* @var resource 资源
|
||||
*/
|
||||
resource
|
||||
|
||||
/**
|
||||
* 发送者ID
|
||||
*/
|
||||
sender_id = 0
|
||||
|
||||
/**
|
||||
* 接收者ID
|
||||
*/
|
||||
receiver_id = 0
|
||||
|
||||
/**
|
||||
* 聊天类型[1:私聊;2:群聊;]
|
||||
*/
|
||||
talk_type = 0
|
||||
|
||||
/**
|
||||
* 初始化构造方法
|
||||
*
|
||||
* @param {Object} resource Socket消息
|
||||
*/
|
||||
constructor(resource) {
|
||||
super()
|
||||
|
||||
this.sender_id = resource.sender_id
|
||||
this.receiver_id = resource.receiver_id
|
||||
this.talk_type = resource.talk_type
|
||||
this.resource = resource.data
|
||||
|
||||
this.handle()
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断消息发送者是否来自于我
|
||||
* @returns
|
||||
*/
|
||||
isCurrSender() {
|
||||
return this.sender_id == this.getAccountId()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话索引
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
getIndexName() {
|
||||
if (this.talk_type == 2) {
|
||||
return `${this.talk_type}_${this.receiver_id}`
|
||||
}
|
||||
|
||||
let receiver_id = this.isCurrSender() ? this.receiver_id : this.sender_id
|
||||
|
||||
return `${this.talk_type}_${receiver_id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天列表左侧的对话信息
|
||||
*/
|
||||
getTalkText() {
|
||||
let text = ''
|
||||
if (this.resource.msg_type != message.ChatMsgTypeText) {
|
||||
text = message.ChatMsgTypeMapping[this.resource.msg_type]
|
||||
} else {
|
||||
text = this.resource.extra.content.replace(/<img .*?>/g, '')
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// 播放提示音
|
||||
play() {
|
||||
// 客户端有消息提示
|
||||
// if (isElectronMode()) return
|
||||
|
||||
// useSettingsStore().isPromptTone && palyMusic()
|
||||
}
|
||||
|
||||
handle() {
|
||||
// 不是自己发送的消息则需要播放提示音
|
||||
if (!this.isCurrSender()) {
|
||||
this.play()
|
||||
}
|
||||
|
||||
// 判断会话列表是否存在,不存在则创建
|
||||
if (useTalkStore().findTalkIndex(this.getIndexName()) == -1) {
|
||||
return this.addTalkItem()
|
||||
}
|
||||
|
||||
// 判断当前是否正在和好友对话
|
||||
if (this.isTalk(this.talk_type, this.receiver_id, this.sender_id)) {
|
||||
this.insertTalkRecord()
|
||||
} else {
|
||||
this.updateTalkItem()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示消息提示
|
||||
* @returns
|
||||
*/
|
||||
showMessageNocice() {
|
||||
// if (useSettingsStore().isLeaveWeb) {
|
||||
// const notification = new Notification('LumenIM 在线聊天', {
|
||||
// dir: 'auto',
|
||||
// lang: 'zh-CN',
|
||||
// body: '您有新的消息请注意查收'
|
||||
// })
|
||||
|
||||
// notification.onclick = () => {
|
||||
// notification.close()
|
||||
// }
|
||||
// } else {
|
||||
// window['$notification'].create({
|
||||
// title: '消息通知',
|
||||
// content: '您有新的消息请注意查收',
|
||||
// duration: 3000
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载对接节点
|
||||
*/
|
||||
addTalkItem() {
|
||||
let receiver_id = this.sender_id
|
||||
let talk_type = this.talk_type
|
||||
|
||||
if (talk_type == 1 && this.receiver_id != this.getAccountId()) {
|
||||
receiver_id = this.receiver_id
|
||||
} else if (talk_type == 2) {
|
||||
receiver_id = this.receiver_id
|
||||
}
|
||||
|
||||
ServeCreateTalkList({
|
||||
talk_type,
|
||||
receiver_id
|
||||
}).then(({ code, data }) => {
|
||||
if (code == 200) {
|
||||
let item = formatTalkItem(data)
|
||||
item.unread_num = 1
|
||||
useTalkStore().addItem(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入对话记录
|
||||
*/
|
||||
insertTalkRecord() {
|
||||
let record = this.resource
|
||||
|
||||
// 群成员变化的消息,需要更新群成员列表
|
||||
if ([1102, 1103, 1104].includes(record.msg_type)) {
|
||||
useDialogueStore().updateGroupMembers()
|
||||
}
|
||||
|
||||
useDialogueStore().addDialogueRecord(formatTalkRecord(this.getAccountId(), this.resource))
|
||||
|
||||
if (!this.isCurrSender()) {
|
||||
// 推送已读消息
|
||||
setTimeout(() => {
|
||||
ws.emit('im.message.read', {
|
||||
receiver_id: this.sender_id,
|
||||
msg_ids: [this.resource.msg_id]
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 获取聊天面板元素节点
|
||||
const el = document.getElementById('imChatPanel')
|
||||
if (!el) return
|
||||
|
||||
// 判断的滚动条是否在底部
|
||||
const isBottom = Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight
|
||||
|
||||
if (isBottom || record.user_id == this.getAccountId()) {
|
||||
nextTick(() => {
|
||||
el.scrollTop = el.scrollHeight + 1000
|
||||
})
|
||||
} else {
|
||||
useDialogueStore().setUnreadBubble()
|
||||
}
|
||||
|
||||
useTalkStore().updateItem({
|
||||
index_name: this.getIndexName(),
|
||||
msg_text: this.getTalkText(),
|
||||
updated_at: parseTime(new Date())
|
||||
})
|
||||
|
||||
if (this.talk_type == 1 && this.getAccountId() !== this.sender_id) {
|
||||
ServeClearTalkUnreadNum({
|
||||
talk_type: 1,
|
||||
receiver_id: this.sender_id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话列表记录
|
||||
*/
|
||||
updateTalkItem() {
|
||||
useTalkStore().updateMessage({
|
||||
index_name: this.getIndexName(),
|
||||
msg_text: this.getTalkText(),
|
||||
updated_at: parseTime(new Date())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Talk
|
@ -1,163 +0,0 @@
|
||||
const cache = new Set()
|
||||
|
||||
const maxAttempts = 100
|
||||
|
||||
const defaultEvent = {
|
||||
onError: (evt: any) => console.error('WebSocket Error:', evt),
|
||||
onOpen: (evt: any) => console.log('WebSocket Opened:', evt),
|
||||
onClose: (evt: any) => console.log('WebSocket Closed:', evt)
|
||||
}
|
||||
|
||||
class WsSocket {
|
||||
connect: WebSocket | null = null
|
||||
|
||||
config: any = {
|
||||
heartbeat: {
|
||||
setInterval: null,
|
||||
pingInterval: 20000,
|
||||
pingTimeout: 60000
|
||||
},
|
||||
reconnect: {
|
||||
lockReconnect: false,
|
||||
setTimeout: null,
|
||||
interval: [2000, 2500, 3000, 3000, 5000, 8000], // Exponential backoff
|
||||
attempts: maxAttempts
|
||||
}
|
||||
}
|
||||
|
||||
lastTime: number = 0
|
||||
|
||||
onCallBacks: Record<string, Function> = {}
|
||||
|
||||
defaultEvent: Record<string, Function> = defaultEvent
|
||||
|
||||
constructor(
|
||||
private urlCallBack: () => string,
|
||||
private events: Partial<typeof defaultEvent>
|
||||
) {
|
||||
this.events = { ...this.defaultEvent, ...events }
|
||||
}
|
||||
|
||||
on(event: string, callback: Function): this {
|
||||
this.onCallBacks[event] = callback
|
||||
return this
|
||||
}
|
||||
|
||||
loadSocket(): void {
|
||||
this.connect = new WebSocket(this.urlCallBack())
|
||||
this.connect.onerror = this.onError.bind(this)
|
||||
this.connect.onopen = this.onOpen.bind(this)
|
||||
this.connect.onmessage = this.onMessage.bind(this)
|
||||
this.connect.onclose = this.onClose.bind(this)
|
||||
}
|
||||
|
||||
connection(): void {
|
||||
this.connect === null && this.loadSocket()
|
||||
}
|
||||
|
||||
reconnect(): void {
|
||||
if (this.config.reconnect.lockReconnect || this.config.reconnect.attempts <= 0) return
|
||||
|
||||
this.config.reconnect.lockReconnect = true
|
||||
this.config.reconnect.attempts--
|
||||
|
||||
const delay = this.config.reconnect.interval.shift()
|
||||
|
||||
this.config.reconnect.setTimeout = setTimeout(() => {
|
||||
console.log(new Date().toLocaleString(), 'Attempting to reconnect to WebSocket...')
|
||||
this.connection()
|
||||
}, delay || 10000)
|
||||
}
|
||||
|
||||
onParse(evt: MessageEvent): any {
|
||||
return JSON.parse(evt.data)
|
||||
}
|
||||
|
||||
onOpen(evt: Event): void {
|
||||
this.lastTime = Date.now()
|
||||
|
||||
this.events.onOpen?.(evt)
|
||||
|
||||
this.config.reconnect.interval = [1000, 1000, 3000, 5000, 10000]
|
||||
this.config.reconnect.lockReconnect = false
|
||||
this.config.reconnect.attempts = maxAttempts
|
||||
|
||||
this.heartbeat()
|
||||
}
|
||||
|
||||
onClose(evt: CloseEvent): void {
|
||||
this.events.onClose?.(evt)
|
||||
this.connect = null
|
||||
|
||||
this.config.heartbeat.setInterval && clearInterval(this.config.heartbeat.setInterval)
|
||||
|
||||
this.config.reconnect.lockReconnect = false
|
||||
|
||||
if (evt.code !== 1000) {
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
onError(evt: Event): void {
|
||||
this.events.onError?.(evt)
|
||||
}
|
||||
|
||||
onMessage(evt: MessageEvent): void {
|
||||
this.lastTime = Date.now()
|
||||
|
||||
const data = this.onParse(evt)
|
||||
|
||||
if (data.event === 'pong') {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.ackid) {
|
||||
this.connect?.send(`{"event":"ack","ackid":"${data.ackid}"}`)
|
||||
|
||||
if (cache.has(data.ackid)) return
|
||||
|
||||
cache.add(data.ackid)
|
||||
}
|
||||
|
||||
if (this.onCallBacks[data.event]) {
|
||||
this.onCallBacks[data.event](data.payload, evt.data)
|
||||
} else {
|
||||
console.warn(`WsSocket message event [${data.event}] not bound...`)
|
||||
}
|
||||
}
|
||||
|
||||
heartbeat(): void {
|
||||
this.config.heartbeat.setInterval && clearInterval(this.config.heartbeat.setInterval)
|
||||
|
||||
this.config.heartbeat.setInterval = setInterval(() => {
|
||||
this.ping()
|
||||
}, this.config.heartbeat.pingInterval)
|
||||
}
|
||||
|
||||
ping(): void {
|
||||
this.connect?.send(JSON.stringify({ event: 'ping' }))
|
||||
}
|
||||
|
||||
send(message: any): void {
|
||||
if (this.connect && this.connect.readyState === WebSocket.OPEN) {
|
||||
this.connect.send(typeof message === 'string' ? message : JSON.stringify(message))
|
||||
} else {
|
||||
alert('WebSocket 连接已关闭')
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.connect?.close()
|
||||
this.config.heartbeat.setInterval && clearInterval(this.config.heartbeat.setInterval)
|
||||
}
|
||||
|
||||
emit(event: string, payload: any): void {
|
||||
if (this.connect && this.connect.readyState === WebSocket.OPEN) {
|
||||
this.connect.send(JSON.stringify({ event, payload }))
|
||||
} else {
|
||||
console.error('WebSocket connection closed...', this.connect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default WsSocket
|
262
src/plugins/ws-socket.js
Normal file
262
src/plugins/ws-socket.js
Normal file
@ -0,0 +1,262 @@
|
||||
const cache = new Set()
|
||||
|
||||
class WsSocket {
|
||||
/**
|
||||
* Websocket 连接
|
||||
*
|
||||
* @var Websocket
|
||||
*/
|
||||
connect
|
||||
|
||||
/**
|
||||
* 配置信息
|
||||
*
|
||||
* @var Object
|
||||
*/
|
||||
config = {
|
||||
heartbeat: {
|
||||
setInterval: null,
|
||||
pingInterval: 20000,
|
||||
pingTimeout: 60000
|
||||
},
|
||||
reconnect: {
|
||||
lockReconnect: false,
|
||||
setTimeout: null, // 计时器对象
|
||||
time: 3000, // 重连间隔时间
|
||||
number: 10000000 // 重连次数
|
||||
}
|
||||
}
|
||||
|
||||
// 最后心跳时间
|
||||
lastTime = 0
|
||||
|
||||
/**
|
||||
* 自定义绑定消息事件
|
||||
*
|
||||
* @var Array
|
||||
*/
|
||||
onCallBacks = []
|
||||
|
||||
defaultEvent = {
|
||||
onError: (evt) => {
|
||||
console.log(evt)
|
||||
},
|
||||
onOpen: (evt) => {
|
||||
console.log(evt)
|
||||
},
|
||||
onClose: (evt) => {
|
||||
console.log(evt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 WsSocket 的实例
|
||||
*
|
||||
* @param {Function} urlCallBack url闭包函数
|
||||
* @param {Object} events 原生 WebSocket 绑定事件
|
||||
*/
|
||||
constructor(urlCallBack, events) {
|
||||
this.urlCallBack = urlCallBack
|
||||
|
||||
// 定义 WebSocket 原生方法
|
||||
this.events = Object.assign({}, this.defaultEvent, events)
|
||||
|
||||
this.on('connect', (data) => {
|
||||
this.config.heartbeat.pingInterval = data.ping_interval * 1000
|
||||
this.config.heartbeat.pingTimeout = data.ping_timeout * 1000
|
||||
this.heartbeat()
|
||||
this.connect.send('{"event":"ping"}')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件绑定
|
||||
*
|
||||
* @param {String} event 事件名
|
||||
* @param {Function} callBack 回调方法
|
||||
*/
|
||||
on(event, callBack) {
|
||||
this.onCallBacks[event] = callBack
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 WebSocket
|
||||
*/
|
||||
loadSocket() {
|
||||
const url = this.urlCallBack()
|
||||
|
||||
const connect = new WebSocket(url)
|
||||
connect.onerror = this.onError.bind(this)
|
||||
connect.onopen = this.onOpen.bind(this)
|
||||
connect.onmessage = this.onMessage.bind(this)
|
||||
connect.onclose = this.onClose.bind(this)
|
||||
|
||||
this.connect = connect
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接 Websocket
|
||||
*/
|
||||
connection() {
|
||||
this.connect == null && this.loadSocket()
|
||||
}
|
||||
|
||||
/**
|
||||
* 掉线重连 Websocket
|
||||
*/
|
||||
reconnect() {
|
||||
// 没连接上会一直重连,设置延迟避免请求过多
|
||||
clearTimeout(this.config.reconnect.setTimeout)
|
||||
|
||||
this.config.reconnect.setTimeout = setTimeout(() => {
|
||||
this.connection()
|
||||
|
||||
console.log(`网络连接已断开,正在尝试重新连接...`)
|
||||
}, this.config.reconnect.time)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析接受的消息
|
||||
*
|
||||
* @param {Object} evt Websocket 消息
|
||||
*/
|
||||
onParse(evt) {
|
||||
const { sid, event, content } = JSON.parse(evt.data)
|
||||
|
||||
return {
|
||||
sid: sid,
|
||||
event: event,
|
||||
data: content,
|
||||
orginData: evt.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开连接
|
||||
*
|
||||
* @param {Object} evt Websocket 消息
|
||||
*/
|
||||
onOpen(evt) {
|
||||
this.lastTime = new Date().getTime()
|
||||
|
||||
this.events.onOpen(evt)
|
||||
|
||||
this.ping()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*
|
||||
* @param {Object} evt Websocket 消息
|
||||
*/
|
||||
onClose(evt) {
|
||||
this.events.onClose(evt)
|
||||
|
||||
this.connect && this.connect.close()
|
||||
|
||||
this.connect = null
|
||||
|
||||
evt.code == 1006 && this.reconnect()
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接错误
|
||||
*
|
||||
* @param {Object} evt Websocket 消息
|
||||
*/
|
||||
onError(evt) {
|
||||
this.events.onError(evt)
|
||||
this.connect.close()
|
||||
this.connect = null
|
||||
this.reconnect()
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收消息
|
||||
*
|
||||
* @param {Object} evt Websocket 消息
|
||||
*/
|
||||
onMessage(evt) {
|
||||
this.lastTime = new Date().getTime()
|
||||
|
||||
let result = this.onParse(evt)
|
||||
|
||||
if (result.sid) {
|
||||
if (cache.has(result.sid)) return
|
||||
|
||||
cache.add(result.sid)
|
||||
|
||||
this.connect.send(`{"event":"ack","sid":"${result.sid}"}`)
|
||||
}
|
||||
|
||||
// 判断消息事件是否被绑定
|
||||
if (Object.prototype.hasOwnProperty.call(this.onCallBacks, result.event)) {
|
||||
this.onCallBacks[result.event](result.data, result.orginData)
|
||||
} else {
|
||||
console.warn(`WsSocket 消息事件[${result.event}]未绑定...`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 心跳检测
|
||||
*/
|
||||
heartbeat() {
|
||||
this.config.heartbeat.setInterval = setInterval(() => {
|
||||
let t = new Date().getTime()
|
||||
|
||||
if (t - this.lastTime > this.config.heartbeat.pingTimeout) {
|
||||
if (this.connect) {
|
||||
this.connect.close()
|
||||
}
|
||||
|
||||
this.reconnect()
|
||||
} else {
|
||||
this.ping()
|
||||
}
|
||||
}, this.config.heartbeat.pingInterval)
|
||||
}
|
||||
|
||||
ping() {
|
||||
this.connect.send('{"event":"ping"}')
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天发送数据
|
||||
*
|
||||
* @param {Object} mesage
|
||||
*/
|
||||
send(mesage) {
|
||||
if (typeof mesage == 'string') {
|
||||
this.connect.send(mesage)
|
||||
} else {
|
||||
this.connect.send(JSON.stringify(mesage))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
close() {
|
||||
this.connect.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送消息
|
||||
*
|
||||
* @param {String} event 事件名
|
||||
* @param {Object} data 数据
|
||||
*/
|
||||
emit(event, data) {
|
||||
const content = JSON.stringify({ event, content: data })
|
||||
|
||||
if (this.connect && this.connect.readyState === 1) {
|
||||
this.connect.send(content)
|
||||
} else {
|
||||
console.error('WebSocket 连接已关闭...', this.connect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default WsSocket
|
8
src/store/index.js
Normal file
8
src/store/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
export * from '@/store/modules/user'
|
||||
// export * from '@/store/modules/settings'
|
||||
export * from '@/store/modules/talk'
|
||||
// export * from '@/store/modules/editor'
|
||||
export * from '@/store/modules/dialogue'
|
||||
// export * from '@/store/modules/editor-draft'
|
||||
// export * from '@/store/modules/uploads'
|
||||
// export * from '@/store/modules/note'
|
231
src/store/modules/dialogue.js
Normal file
231
src/store/modules/dialogue.js
Normal file
@ -0,0 +1,231 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import {
|
||||
ServeRemoveRecords,
|
||||
ServeRevokeRecords,
|
||||
ServePublishMessage,
|
||||
ServeCollectEmoticon
|
||||
} from '@/api/chat/index'
|
||||
import { ServeGetGroupMembers } from '@/api/group/index'
|
||||
import { useEditorStore } from './editor'
|
||||
|
||||
// 键盘消息事件定时器
|
||||
// let keyboardTimeout = null
|
||||
|
||||
export const useDialogueStore = defineStore('dialogue', {
|
||||
state: () => {
|
||||
return {
|
||||
// 对话索引(聊天对话的唯一索引)
|
||||
index_name: '',
|
||||
|
||||
// 对话节点
|
||||
talk: {
|
||||
username: '',
|
||||
talk_type: 0, // 对话来源[1:私聊;2:群聊]
|
||||
receiver_id: 0
|
||||
},
|
||||
|
||||
// 好友是否正在输入文字
|
||||
keyboard: false,
|
||||
|
||||
// 对方是否在线
|
||||
online: false,
|
||||
|
||||
// 聊天记录
|
||||
records: [],
|
||||
|
||||
// 新消息提示
|
||||
unreadBubble: 0,
|
||||
|
||||
// 是否开启多选操作模式
|
||||
isOpenMultiSelect: false,
|
||||
|
||||
// 是否显示编辑器
|
||||
isShowEditor: false,
|
||||
|
||||
// 是否显示会话列表
|
||||
isShowSessionList: true,
|
||||
|
||||
// 群成员列表
|
||||
members: [],
|
||||
|
||||
// 对话记录
|
||||
items: {
|
||||
'1_1': {
|
||||
talk_type: 1, // 对话类型
|
||||
receiver_id: 0, // 接收者ID
|
||||
read_sequence: 0, // 当前已读的最后一条记录
|
||||
records: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
// 多选列表
|
||||
selectItems: (state) => state.records.filter((item) => item.isCheck),
|
||||
// 当前对话是否是群聊
|
||||
isGroupTalk: (state) => state.talk.talk_type === 2
|
||||
},
|
||||
actions: {
|
||||
// 更新在线状态
|
||||
setOnlineStatus(status) {
|
||||
this.online = status
|
||||
},
|
||||
|
||||
// 更新对话信息
|
||||
setDialogue(data = {}) {
|
||||
this.online = data.is_online == 1
|
||||
this.talk = {
|
||||
username: data.remark || data.name,
|
||||
talk_type: data.talk_type,
|
||||
receiver_id: data.receiver_id
|
||||
}
|
||||
|
||||
this.index_name = `${data.talk_type}_${data.receiver_id}`
|
||||
this.records = []
|
||||
this.unreadBubble = 0
|
||||
this.isShowEditor = data?.is_robot === 0
|
||||
|
||||
this.members = []
|
||||
if (data.talk_type == 2) {
|
||||
this.updateGroupMembers()
|
||||
}
|
||||
},
|
||||
|
||||
// 更新提及列表
|
||||
async updateGroupMembers() {
|
||||
let { code, data } = await ServeGetGroupMembers({
|
||||
group_id: this.talk.receiver_id
|
||||
})
|
||||
|
||||
if (code != 200) return
|
||||
|
||||
this.members = data.items.map((o) => ({
|
||||
id: o.user_id,
|
||||
nickname: o.nickname,
|
||||
avatar: o.avatar,
|
||||
gender: o.gender,
|
||||
leader: o.leader,
|
||||
remark: o.remark,
|
||||
online: false,
|
||||
value: o.nickname
|
||||
}))
|
||||
},
|
||||
|
||||
// 清空对话记录
|
||||
clearDialogueRecord() {
|
||||
this.records = []
|
||||
},
|
||||
|
||||
// 数组头部压入对话记录
|
||||
unshiftDialogueRecord(records) {
|
||||
this.records.unshift(...records)
|
||||
},
|
||||
|
||||
// 推送对话记录
|
||||
addDialogueRecord(record) {
|
||||
// TOOD 需要通过 sequence 排序,保证消息一致性
|
||||
// this.records.splice(index, 0, record)
|
||||
|
||||
this.records.push(record)
|
||||
},
|
||||
|
||||
// 更新对话记录
|
||||
updateDialogueRecord(params) {
|
||||
const { msg_id = '' } = params
|
||||
|
||||
const item = this.records.find((item) => item.msg_id === msg_id)
|
||||
|
||||
item && Object.assign(item, params)
|
||||
},
|
||||
|
||||
// 批量删除对话记录
|
||||
batchDelDialogueRecord(msgIds = []) {
|
||||
msgIds.forEach((msgid) => {
|
||||
const index = this.records.findIndex((item) => item.msg_id === msgid)
|
||||
|
||||
if (index >= 0) this.records.splice(index, 1)
|
||||
})
|
||||
},
|
||||
|
||||
// 自增好友键盘输入事件
|
||||
// triggerKeyboard() {
|
||||
// this.keyboard = true
|
||||
|
||||
// clearTimeout(keyboardTimeout)
|
||||
|
||||
// keyboardTimeout = setTimeout(() => (this.keyboard = false), 2000)
|
||||
// },
|
||||
|
||||
setUnreadBubble(value) {
|
||||
if (value === 0) {
|
||||
this.unreadBubble = 0
|
||||
} else {
|
||||
this.unreadBubble++
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭多选模式
|
||||
closeMultiSelect() {
|
||||
this.isOpenMultiSelect = false
|
||||
|
||||
for (const item of this.selectItems) {
|
||||
if (item.isCheck) {
|
||||
item.isCheck = false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 删除聊天记录
|
||||
ApiDeleteRecord(msgIds = []) {
|
||||
ServeRemoveRecords({
|
||||
talk_type: this.talk.talk_type,
|
||||
receiver_id: this.talk.receiver_id,
|
||||
msg_ids: msgIds
|
||||
}).then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.batchDelDialogueRecord(msgIds)
|
||||
} else {
|
||||
window['$message'].warning(res.message)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 撤销聊天记录
|
||||
ApiRevokeRecord(msg_id = '') {
|
||||
ServeRevokeRecords({ msg_id }).then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.updateDialogueRecord({ msg_id, is_revoke: 1 })
|
||||
} else {
|
||||
window['$message'].warning(res.message)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 转发聊天记录
|
||||
ApiForwardRecord(options) {
|
||||
let data = {
|
||||
type: 'forward',
|
||||
receiver: {
|
||||
talk_type: this.talk.talk_type,
|
||||
receiver_id: this.talk.receiver_id
|
||||
},
|
||||
...options
|
||||
}
|
||||
|
||||
ServePublishMessage(data).then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.closeMultiSelect()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
ApiCollectImage(options) {
|
||||
const { msg_id } = options
|
||||
|
||||
ServeCollectEmoticon({ msg_id }).then(() => {
|
||||
useEditorStore().loadUserEmoticon()
|
||||
window['$message'] && window['$message'].success('收藏成功')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
13
src/store/modules/editor-draft.js
Normal file
13
src/store/modules/editor-draft.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
// 编辑器草稿
|
||||
export const useEditorDraftStore = defineStore('editor-draft', {
|
||||
// 开启数据持久化
|
||||
persist: true,
|
||||
state: () => {
|
||||
return {
|
||||
items: {}
|
||||
}
|
||||
},
|
||||
actions: {}
|
||||
})
|
80
src/store/modules/editor.js
Normal file
80
src/store/modules/editor.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ServeFindUserEmoticon, ServeUploadEmoticon, ServeDeleteEmoticon } from '@/api/emoticon/index'
|
||||
import { ServeCollectEmoticon } from '@/api/chat/index'
|
||||
|
||||
const message = window['$message']
|
||||
|
||||
export const useEditorStore = defineStore('editor', {
|
||||
state: () => {
|
||||
return {
|
||||
// 表包相关
|
||||
emoticon: {
|
||||
items: [
|
||||
{
|
||||
name: '系统表情',
|
||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAAAzFBMVEUAAAD7ywP7ywT80wH6wwb6xgX80wH6wwb7zwL7zgP5xAb7ywT5xAb5wgf7ywT80gH80gL7ywP80gH7ywT7zgP6yAX80wH5wQf80QL80wH6xgb81AH5wgf6xwX81AH5wQf81AH5wwb80gL5wQf7zgP5wgf80gH7zAP5wwb6ygT6xwX70AL7zgOudAD6xQb2wgXVnwLwvwPRmgLDigG1ewCxdwDgqQTcpgPYoQPrvALhsAG6gQHosgTntAP1yAPMlQLJkgLJkQK/hgG3fgAADXCMAAAAJnRSTlMATgT7znJxcHAM/fnz7uzs09HMqS4mJfry8u3t1tXVqqpcWy0tD14bIcIAAAGzSURBVDjLnZTXcsIwEEUdcAFCCy290GwgLHKld/7/n6IVljBGDgznxbtXZzSz9niVO+noP+X3dPq9/KN3kq2s/mVG+NKzUi2VKZkxSpnUpdeomBIqjbj38GpKeX0491ppM4F06+w+9JLMyJ2NF/MfXhpiXjGHnAqfPfN7hUz4nkvXxFKWiboI5u7sdDpz56LWmVgehswBgjVv1gHAnDdl9GpDjg0AM97MaGOLoxqOIjrHB8/ijeWB74gjHKdqCfqOFcHpn+oqFZ+sG3iiYvEWsUjFx1vERxT7N4Bi8VgOpAKPizgMFuQQrGTeKjgQfOIwVSyWAP5Acp8PwC7A16MNKCQAsAcX2AABwULDT8iiKQBM4t6EhlNW1RTKJys9Gm5IVCMbGnms/FQQ7RjvaezbDtcc26fBnrBGY2L2bYSQLSCLnTudursFIFvCjt7ClWGMjowXcMZiHB4Y/OdSw6A39kDgjXthrIrFUs/3OGQ5sV3XniyJiPL1yALI9RLJnS2VZi7Ra8aWVF7u5fl9groq89S6ZJEahbhWMFKKjK72EdU+tK6SSNv4VgvPzwX122gr9/EHUym1IM88uoYAAAAASUVORK5CYII='
|
||||
},
|
||||
{
|
||||
name: '我的收藏',
|
||||
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAC1klEQVRYw+2XX0hTcRTHP3eOCgOtNCOI/lBYvQSNXtL8t0yRgnI3yjCil7AQkggignwoiN7ypYcopB5cFtypkYRmm38yX6Sgl6YQRQSRpqWUhOhOD9vcmnO781p3D/vCYez3O7/z+9zf+e3uHEgpJXOlLDQhqmoB7MBhIA/YDKQDI8A7wAM0KZr2OdYGoqobgGqgBNgJ5ABTwEdgAGgF3Iqm+XQDiqqqwA0gN84DzgLNwBVF0z5FxNgYiFEFpMWJMwxcUjStLSagz+FYDtwDTiaYiZ9AncXlagzEqQLuABkJxmkEzllcrul5gD6Hwwo8ASoSDBquW4APuGggRhtw1OJyzQBYg6MiUm8QDuCCwfXgv/P1AfOf4Gzlka2ANxzYZM0AO9JaWt9bAEQ4L4JVBJLErCLUETwxETlo9pFFUcUcIMgWs2miaNMcoIhYzKaJoulwwAkg02yiCE2FAJHRJAQcDwGKeIFtZhNFaAjAf/dE+s1+r0SxgTlAEWkXEZLM2iHsv/hXRflrYLfZeQ3ozcpnHbZQigGBBvF/JoM1BLlCgCIPRcSbBKn1ikhzkOuvenCy/EAZ0GFyesszOp53RgUEmCgrdQInTIJzZnZ2VYcPzC+vRGoAG7D9P8MNAWcjB6P2JD9K7bvwNzTp/wluCti7qsv9VhcgwPfSkkOABiz7x3DTgLq6y/M02qQSa+X4/uJq4AHxu7LFygecWvOiu2khByVehDF70Rn8HVpc3wQlQE2Wu+duLCddm47ZC48D94EVSwT3Gzid5e59FM9R96l8KynYAzwGjFbfH4Bj2Z6+QT3OuivpbE/foAg2EZwGihSnCDa9cAmdYLhGi/dVAreB9TqXfAFq13a/bEl0r0Vf/JGi/AzgGlDLwv30TOBB6nN6+icXs4/hX+ZIUX4ucBOojJhqAS7n9PQPG4m/ZK+Or4V5BcD1wNer63pf9S1V7JRSMlN/AO6lbgA7RvQLAAAAAElFTkSuQmCC',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// 加载用户表情包
|
||||
loadUserEmoticon() {
|
||||
ServeFindUserEmoticon().then((res) => {
|
||||
if (res.code == 200) {
|
||||
const { collect_emoticon } = res.data
|
||||
|
||||
// 用户收藏的系统表情包
|
||||
this.emoticon.items[1].children = collect_emoticon || []
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 收藏用户表情包
|
||||
saveUserEmoticon(resoure) {
|
||||
ServeCollectEmoticon({
|
||||
record_id: resoure.record_id
|
||||
}).then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.loadUserEmoticon()
|
||||
} else {
|
||||
message.warning(res.message)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 自定义上传用户表情包
|
||||
uploadUserEmoticon(file) {
|
||||
const data = new FormData()
|
||||
data.append('emoticon', file)
|
||||
|
||||
ServeUploadEmoticon(data).then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.emoticon.items[1].children.unshift(res.data)
|
||||
} else {
|
||||
message.warning(res.message)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 自定义上传用户表情包
|
||||
removeUserEmoticon(resoure) {
|
||||
ServeDeleteEmoticon({
|
||||
ids: [resoure.id].join(',')
|
||||
}).then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.emoticon.items[1].children.splice(resoure.index, 1)
|
||||
message.success('删除成功')
|
||||
} else {
|
||||
message.warning(res.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
151
src/store/modules/talk.js
Normal file
151
src/store/modules/talk.js
Normal file
@ -0,0 +1,151 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ServeGetTalkList, ServeCreateTalkList } from '@/api/chat/index'
|
||||
import { formatTalkItem, ttime, KEY_INDEX_NAME } from '@/utils/talk'
|
||||
import { useEditorDraftStore } from './editor-draft'
|
||||
// import { ISession } from '@/types/chat'
|
||||
|
||||
export const useTalkStore = defineStore('talk', {
|
||||
state: () => {
|
||||
return {
|
||||
// 加载状态[1:未加载;2:加载中;3:加载完成;4:加载失败;]
|
||||
loadStatus: 2,
|
||||
|
||||
// 会话列表
|
||||
items: []
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
// 过滤所有置顶对话列表
|
||||
topItems: (state) => {
|
||||
return state.items.filter((item) => item.is_top == 1)
|
||||
},
|
||||
|
||||
// 对话列表
|
||||
talkItems: (state) => {
|
||||
return state.items.sort((a, b) => {
|
||||
return ttime(b.updated_at) - ttime(a.updated_at)
|
||||
})
|
||||
},
|
||||
|
||||
// 消息未读数总计
|
||||
talkUnreadNum: (state) => {
|
||||
return state.items.reduce((total, item) => {
|
||||
return total + item.unread_num
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
findItem(index_name) {
|
||||
return this.items.find((item) => item.index_name === index_name)
|
||||
},
|
||||
|
||||
// 更新对话节点
|
||||
updateItem(params) {
|
||||
const item = this.items.find((item) => item.index_name === params.index_name)
|
||||
|
||||
item && Object.assign(item, params)
|
||||
},
|
||||
|
||||
// 新增对话节点
|
||||
addItem(params) {
|
||||
this.items = [params, ...this.items]
|
||||
},
|
||||
|
||||
// 移除对话节点
|
||||
delItem(index_name) {
|
||||
const i = this.items.findIndex((item) => item.index_name === index_name)
|
||||
|
||||
if (i >= 0) {
|
||||
this.items.splice(i, 1)
|
||||
}
|
||||
|
||||
this.items = [...this.items]
|
||||
},
|
||||
|
||||
// 更新对话消息
|
||||
updateMessage(params) {
|
||||
const item = this.items.find((item) => item.index_name === params.index_name)
|
||||
|
||||
if (item) {
|
||||
item.unread_num++
|
||||
// item.msg_text = params.msg_text
|
||||
item.updated_at = params.updated_at
|
||||
}
|
||||
},
|
||||
|
||||
// 更新联系人备注
|
||||
setRemark(params) {
|
||||
const item = this.items.find((item) => item.index_name === `1_${params.user_id}`)
|
||||
|
||||
item && (item.remark = params.remark)
|
||||
},
|
||||
|
||||
// 加载会话列表
|
||||
loadTalkList() {
|
||||
this.loadStatus = 2
|
||||
|
||||
const resp = ServeGetTalkList()
|
||||
|
||||
resp.then(({ code, data }) => {
|
||||
if (code == 200) {
|
||||
this.items = data.items.map((item) => {
|
||||
const value = formatTalkItem(item)
|
||||
|
||||
const draft = useEditorDraftStore().items[value.index_name]
|
||||
if (draft) {
|
||||
value.draft_text = JSON.parse(draft).text || ''
|
||||
}
|
||||
|
||||
if (value.is_robot == 1) {
|
||||
value.is_online = 1
|
||||
}
|
||||
|
||||
return value
|
||||
})
|
||||
|
||||
this.loadStatus = 3
|
||||
} else {
|
||||
this.loadStatus = 4
|
||||
}
|
||||
})
|
||||
|
||||
resp.catch(() => {
|
||||
this.loadStatus = 4
|
||||
})
|
||||
},
|
||||
|
||||
findTalkIndex(index_name) {
|
||||
return this.items.findIndex((item) => item.index_name === index_name)
|
||||
},
|
||||
|
||||
toTalk(talk_type, receiver_id, router) {
|
||||
const route = {
|
||||
path: '/message',
|
||||
query: {
|
||||
v: new Date().getTime()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.findTalkIndex(`${talk_type}_${receiver_id}`) >= 0) {
|
||||
sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`)
|
||||
return router.push(route)
|
||||
}
|
||||
|
||||
ServeCreateTalkList({
|
||||
talk_type,
|
||||
receiver_id
|
||||
}).then(({ code, data, message }) => {
|
||||
if (code == 200) {
|
||||
if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) {
|
||||
this.addItem(formatTalkItem(data))
|
||||
}
|
||||
|
||||
sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`)
|
||||
return router.push(route)
|
||||
} else {
|
||||
window['$message'].info(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
70
src/store/modules/user.js
Normal file
70
src/store/modules/user.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
// import { ServeGetUserSetting } from '@/api/user'
|
||||
// import { ServeFindFriendApplyNum } from '@/api/contact'
|
||||
import { ServeGroupApplyUnread } from '@/api/group'
|
||||
// import { delAccessToken } from '@/utils/auth'
|
||||
// import { storage } from '@/utils/storage'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
persist: true,
|
||||
state: () => {
|
||||
return {
|
||||
uid: 0, // 用户ID
|
||||
mobile: '',
|
||||
email: '',
|
||||
nickname: '', // 用户昵称
|
||||
gender: 0, // 性别
|
||||
motto: '', // 个性签名
|
||||
avatar: '',
|
||||
banner: '', // 名片背景
|
||||
online: false, // 在线状态
|
||||
isQiye: false,
|
||||
isContactApply: false,
|
||||
isGroupApply: false
|
||||
}
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
// 设置用户登录状态
|
||||
updateSocketStatus(status) {
|
||||
this.online = status
|
||||
},
|
||||
|
||||
// logoutLogin() {
|
||||
// this.$reset()
|
||||
// storage.remove('user_info')
|
||||
// delAccessToken()
|
||||
// location.reload()
|
||||
// },
|
||||
|
||||
loadSetting() {
|
||||
// ServeGetUserSetting().then(({ code, data }) => {
|
||||
// if (code == 200) {
|
||||
// this.nickname = data.user_info.nickname
|
||||
// this.uid = data.user_info.uid
|
||||
// this.avatar = data.user_info.avatar
|
||||
|
||||
// this.gender = data.user_info.gender
|
||||
// this.mobile = data.user_info.mobile || ''
|
||||
// this.email = data.user_info.email || ''
|
||||
// this.motto = data.user_info.motto
|
||||
// this.isQiye = data.user_info.is_qiye || false
|
||||
|
||||
// storage.set('user_info', data)
|
||||
// }
|
||||
// })
|
||||
|
||||
// ServeFindFriendApplyNum().then(({ code, data }) => {
|
||||
// if (code == 200) {
|
||||
// this.isContactApply = data.unread_num > 0
|
||||
// }
|
||||
// })
|
||||
|
||||
ServeGroupApplyUnread().then(({ code, data }) => {
|
||||
if (code == 200) {
|
||||
this.isGroupApply = data.unread_num > 0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
158
src/utils/datetime.js
Normal file
158
src/utils/datetime.js
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 人性化时间显示
|
||||
*
|
||||
* @param {Object} datetime
|
||||
*/
|
||||
export function formatTime(datetime) {
|
||||
if (datetime == null) return ''
|
||||
|
||||
datetime = datetime.replace(/-/g, '/')
|
||||
|
||||
let time = new Date()
|
||||
let outTime = new Date(datetime)
|
||||
if (/^[1-9]\d*$/.test(datetime)) {
|
||||
outTime = new Date(parseInt(datetime) * 1000)
|
||||
}
|
||||
|
||||
if (time.getTime() < outTime.getTime() || time.getFullYear() != outTime.getFullYear()) {
|
||||
return parseTime(outTime, '{y}/{m}/{d} {h}:{i}')
|
||||
}
|
||||
|
||||
if (time.getMonth() != outTime.getMonth()) {
|
||||
return parseTime(outTime, '{m}/{d} {h}:{i}')
|
||||
}
|
||||
|
||||
if (time.getDate() != outTime.getDate()) {
|
||||
let day = outTime.getDate() - time.getDate()
|
||||
if (day == -1) {
|
||||
return parseTime(outTime, '昨天 {h}:{i}')
|
||||
}
|
||||
|
||||
if (day == -2) {
|
||||
return parseTime(outTime, '前天 {h}:{i}')
|
||||
}
|
||||
|
||||
return parseTime(outTime, '{m}-{d} {h}:{i}')
|
||||
}
|
||||
|
||||
let diff = time.getTime() - outTime.getTime()
|
||||
|
||||
if (time.getHours() != outTime.getHours() || diff > 30 * 60 * 1000) {
|
||||
return parseTime(outTime, '{h}:{i}')
|
||||
}
|
||||
|
||||
let minutes = outTime.getMinutes() - time.getMinutes()
|
||||
if (minutes == 0) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
minutes = Math.abs(minutes)
|
||||
return `${minutes}分钟前`
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间格式化方法
|
||||
*
|
||||
* @param {(Object|string|number)} time
|
||||
* @param {String} cFormat
|
||||
* @returns {String | null}
|
||||
*/
|
||||
export function parseTime(time, cFormat) {
|
||||
if (arguments.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
let date
|
||||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
|
||||
|
||||
if (typeof time === 'object') {
|
||||
date = time
|
||||
} else {
|
||||
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
|
||||
time = parseInt(time)
|
||||
}
|
||||
if (typeof time === 'number' && time.toString().length === 10) {
|
||||
time = time * 1000
|
||||
}
|
||||
|
||||
date = new Date(time.replace(/-/g, '/'))
|
||||
}
|
||||
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay()
|
||||
}
|
||||
|
||||
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
|
||||
const value = formatObj[key]
|
||||
// Note: getDay() returns 0 on Sunday
|
||||
if (key === 'a') {
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][value]
|
||||
}
|
||||
|
||||
return value.toString().padStart(2, '0')
|
||||
})
|
||||
|
||||
return time_str
|
||||
}
|
||||
|
||||
/**
|
||||
* 人性化显示时间
|
||||
*
|
||||
* @param {Object} datetime
|
||||
*/
|
||||
export function beautifyTime(datetime = '') {
|
||||
if (datetime == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
datetime = datetime.replace(/-/g, '/')
|
||||
|
||||
let time = new Date()
|
||||
let outTime = new Date(datetime)
|
||||
if (/^[1-9]\d*$/.test(datetime)) {
|
||||
outTime = new Date(parseInt(datetime) * 1000)
|
||||
}
|
||||
|
||||
if (time.getTime() < outTime.getTime()) {
|
||||
return parseTime(outTime, '{y}/{m}/{d}')
|
||||
}
|
||||
|
||||
if (time.getFullYear() != outTime.getFullYear()) {
|
||||
return parseTime(outTime, '{y}/{m}/{d}')
|
||||
}
|
||||
|
||||
if (time.getMonth() != outTime.getMonth()) {
|
||||
return parseTime(outTime, '{m}/{d}')
|
||||
}
|
||||
|
||||
if (time.getDate() != outTime.getDate()) {
|
||||
let day = outTime.getDate() - time.getDate()
|
||||
if (day == -1) {
|
||||
return parseTime(outTime, '昨天 {h}:{i}')
|
||||
}
|
||||
|
||||
if (day == -2) {
|
||||
return parseTime(outTime, '前天 {h}:{i}')
|
||||
}
|
||||
|
||||
return parseTime(outTime, '{m}-{d}')
|
||||
}
|
||||
|
||||
if (time.getHours() != outTime.getHours()) {
|
||||
return parseTime(outTime, '{h}:{i}')
|
||||
}
|
||||
|
||||
let minutes = outTime.getMinutes() - time.getMinutes()
|
||||
if (minutes == 0) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
minutes = Math.abs(minutes)
|
||||
return `${minutes}分钟前`
|
||||
}
|
86
src/utils/talk.js
Normal file
86
src/utils/talk.js
Normal file
@ -0,0 +1,86 @@
|
||||
import { parseTime } from './datetime'
|
||||
|
||||
export const KEY_INDEX_NAME = 'send_message_index_name'
|
||||
|
||||
export function formatTalkRecord(uid, data) {
|
||||
data.float = 'center'
|
||||
|
||||
if (data.user_id > 0) {
|
||||
data.float = data.user_id == uid ? 'right' : 'left'
|
||||
}
|
||||
|
||||
data.isCheck = false
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// 播放消息提示
|
||||
export function palyMusic(muted = false) {
|
||||
let audio = document.getElementById('audio')
|
||||
audio.currentTime = 0
|
||||
audio.muted = muted
|
||||
audio.play()
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化聊天对话列表数据
|
||||
*
|
||||
* @param {Object} params
|
||||
*/
|
||||
export function formatTalkItem(params) {
|
||||
let options = {
|
||||
id: 0,
|
||||
talk_type: 1,
|
||||
receiver_id: 0,
|
||||
name: '未设置',
|
||||
remark: '',
|
||||
avatar: '',
|
||||
is_disturb: 0,
|
||||
is_top: 0,
|
||||
is_online: 0,
|
||||
is_robot: 0,
|
||||
unread_num: 0,
|
||||
content: '......',
|
||||
draft_text: '',
|
||||
msg_text: '',
|
||||
index_name: '',
|
||||
updated_at: parseTime(new Date())
|
||||
}
|
||||
|
||||
options = { ...options, ...params }
|
||||
options.index_name = `${options.talk_type}_${options.receiver_id}`
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要打开的对话索引值
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export function getCacheIndexName() {
|
||||
let index_name = sessionStorage.getItem(KEY_INDEX_NAME)
|
||||
|
||||
if (index_name) {
|
||||
sessionStorage.removeItem(KEY_INDEX_NAME)
|
||||
}
|
||||
|
||||
return index_name
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要打开的对话索引值
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export function setCacheIndexName(type, id) {
|
||||
sessionStorage.setItem(KEY_INDEX_NAME, `${type}_${id}`)
|
||||
}
|
||||
|
||||
export const ttime = (datetime) => {
|
||||
if (datetime == undefined || datetime == '') {
|
||||
return new Date().getTime()
|
||||
}
|
||||
|
||||
return new Date(datetime.replace(/-/g, '/')).getTime()
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
"./src/*.ts",
|
||||
"./src/*.d.ts",
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
, "src/connect.js", "src/store/index.js", "src/store/modules/settings.js", "src/store/modules/user.js", "src/store/modules/uploads.js", "src/store/modules/talk.js", "src/event/talk.js", "src/plugins/ws-socket.js" ],
|
||||
"vueCompilerOptions": {
|
||||
"experimentalRuntimeMode": "runtime-uni-app",
|
||||
"nativeTags": ["block", "component", "template", "slot"]
|
||||
|
Loading…
Reference in New Issue
Block a user