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

This commit is contained in:
caiyx 2024-11-20 09:17:47 +08:00
parent aab593f281
commit fd060743bf
21 changed files with 2160 additions and 165 deletions

5
env/.env.test vendored
View File

@ -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'

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@ -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
View 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
View 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'

View 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('收藏成功')
})
}
}
})

View File

@ -0,0 +1,13 @@
import { defineStore } from 'pinia'
// 编辑器草稿
export const useEditorDraftStore = defineStore('editor-draft', {
// 开启数据持久化
persist: true,
state: () => {
return {
items: {}
}
},
actions: {}
})

View 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
View 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
View 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
View 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
View 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()
}

View File

@ -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"]