feat(im): 初始化 useMessageSender.ts

im
YunaiV 2026-04-26 15:56:24 +08:00
parent e573462cb7
commit 2c1ff59286
2 changed files with 227 additions and 2 deletions

View File

@ -125,8 +125,8 @@ export const useMessagePuller = () => {
break
}
// 逐条 dispatch原消息走 insertMessageRECALL 信号走 recallMessage 把同批内已 insert 的原消息翻成撤回提示。
// 后端按 id 升序返回,且信号 id 一定 > 原消息 id status 再插信号所以原消息一定先到、recallMessage 找得到
// 逐条 dispatch原消息走 insertMessageRECALL 信号走 recallMessage 把同批内已 insert 的原消息更新为撤回提示。
// 后端按 id 升序返回,且信号 id 一定 > 原消息 id更新 status 再插信号所以原消息一定先到、recallMessage 找得到
for (const raw of list) {
if (isPrivate) {
const message = raw as ImPrivateMessageRespVO

View File

@ -0,0 +1,225 @@
import { useConversationStore } from '../store/conversationStore'
import {
sendPrivateMessage as apiSendPrivateMessage,
readPrivateMessages as apiReadPrivateMessages,
getPrivateMaxReadMessageId as apiGetPrivateMaxReadMessageId,
recallPrivateMessage as apiRecallPrivateMessage
} from '@/api/im/message/private'
import {
sendGroupMessage as apiSendGroupMessage,
readGroupMessages as apiReadGroupMessages,
recallGroupMessage as apiRecallGroupMessage
} from '@/api/im/message/group'
import { generateClientMessageId, serializeMessage, type TextMessage } from '../../utils/message'
import { ImMessageType, ImMessageStatus, ImConversationType } from '../../utils/constants'
import type { Message } from '../types'
import { useUserStore } from '@/store/modules/user'
/** 非文本消息的扩展选项(通用) */
interface SendExtOptions {
atUserIds?: number[] // 群聊 @ 的用户编号列表
needReceipt?: boolean // 是否需要群回执(默认 false
targetId?: number // 覆盖默认的 targetId
}
/**
* / /
*
*
* 1. / conversation.type
* 2. insertMessage SENDING ackMessage UNREAD FAILED
* 3. WebSocket RECALL websocketStore 退
* 4.
*/
export const useMessageSender = () => {
const conversationStore = useConversationStore()
const userStore = useUserStore()
/** 构造本地乐观消息对象id=0 表示尚未拿到服务端消息 id */
const buildLocalMessage = (opts: {
clientMessageId: string
content: string
targetId: number
type: number
atUserIds?: number[]
}): Message => ({
id: 0,
clientMessageId: opts.clientMessageId,
type: opts.type,
content: opts.content,
status: ImMessageStatus.SENDING,
sendTime: Date.now(),
senderId: Number(userStore.getUser?.id) || 0,
senderNickName: userStore.getUser?.nickname || '',
targetId: opts.targetId,
selfSend: true,
atUserIds: opts.atUserIds
})
/**
*
* 1.
* 2. type / content
*/
const sendRaw = async (type: number, content: string, options?: SendExtOptions) => {
// 1. 参数校验:必须有激活会话和目标 id
const conversation = conversationStore.activeConversation
if (!conversation) {
return
}
const realTarget = options?.targetId || conversation.targetId
if (!realTarget) {
return
}
// 2. 构造本地消息并乐观插入会话;状态先置 SENDING请求结果回来由 ackMessage 更新
const clientMessageId = generateClientMessageId()
const message = buildLocalMessage({
clientMessageId,
content,
targetId: realTarget,
type,
atUserIds: options?.atUserIds
})
const conversationInfo = {
type: conversation.type,
targetId: realTarget,
name: conversation.name || String(realTarget),
avatar: conversation.avatar || ''
}
conversationStore.insertMessage(conversationInfo, message)
// 3. 发送请求:按会话类型分发到不同接口;成功后 ackMessage 更新为 UNREAD失败更新为 FAILED
try {
if (conversation.type === ImConversationType.PRIVATE) {
const data = await apiSendPrivateMessage({
clientMessageId,
receiverId: realTarget,
type,
content
})
conversationStore.ackMessage(conversation.type, realTarget, clientMessageId, {
id: data.id,
sendTime: new Date(data.sendTime).getTime(),
status: data.status
})
} else if (conversation.type === ImConversationType.GROUP) {
const data = await apiSendGroupMessage({
clientMessageId,
groupId: realTarget,
type,
content,
atUserIds: options?.atUserIds,
receipt: options?.needReceipt
})
conversationStore.ackMessage(conversation.type, realTarget, clientMessageId, {
id: data.id,
sendTime: new Date(data.sendTime).getTime(),
status: data.status,
receiptStatus: data.receiptStatus,
readCount: data.readCount
})
}
} catch (e) {
console.error('[IM] 消息发送失败', { type, realTarget, clientMessageId }, e)
conversationStore.ackMessage(conversation.type, realTarget, clientMessageId, {
status: ImMessageStatus.FAILED
})
}
}
/** 发送文本消息最常用的快捷入口MessageInput.vue 文本回车走这里 */
const send = async (text: string, options?: SendExtOptions) => {
if (!text.trim()) {
return
}
await sendRaw(ImMessageType.TEXT, serializeMessage<TextMessage>({ content: text }), options)
}
/**
*
* 1. WebSocket RECALL UI websocketStore
* 2. 退
*/
const recall = async (message: Message) => {
// 参数校验本地占位消息id=0不能撤回
if (!message.id) {
return
}
const conversation = conversationStore.activeConversation
if (!conversation) {
return
}
// 私聊 / 群聊接口签名一致,按会话类型分发
const isPrivate = conversation.type === ImConversationType.PRIVATE
try {
await (isPrivate ? apiRecallPrivateMessage(message.id) : apiRecallGroupMessage(message.id))
} catch (e) {
console.error('[IM] 撤回失败', { messageId: message.id, type: conversation.type }, e)
}
}
/**
* /
* 1.
* 2. idid=0
*/
const readActive = async () => {
const conversation = conversationStore.activeConversation
if (!conversation) {
return
}
// 本地标记已读:未读数清零 + 消息状态更新为 READUI 立刻响应)
conversationStore.markActiveAsRead()
const maxMessageId = conversationStore.getActiveMessages.reduce<number>(
(max, m) => (m.id > max ? m.id : max),
0
)
if (!maxMessageId) {
return
}
// 接口调用:私聊 / 群聊接口签名一致,按会话类型分发;失败仅记录日志,不回退本地已读状态
const isPrivate = conversation.type === ImConversationType.PRIVATE
try {
await (isPrivate
? apiReadPrivateMessages(conversation.targetId, maxMessageId)
: apiReadGroupMessages(conversation.targetId, maxMessageId))
} catch (e) {
console.error(
'[IM] 标记已读失败',
{ type: conversation.type, targetId: conversation.targetId, maxMessageId },
e
)
}
}
/**
*
*
* 1. 线 / RECEIPT 线
* maxReadId status
* 2. 使 readCount / receiptStatus 线
*/
const syncPrivateReadStatus = async (peerId: number) => {
if (!peerId) {
return
}
try {
// 拉取对方已读到的最大消息 id
const maxReadId = await apiGetPrivateMaxReadMessageId(peerId)
if (!maxReadId) {
return
}
// applyReadReceipt 内部把 ≤ maxReadId 的本端消息更新为 READ
conversationStore.applyReadReceipt({
conversationType: ImConversationType.PRIVATE,
targetId: peerId,
privateReadMaxId: maxReadId
})
} catch (e) {
console.warn('[IM] 拉取对方已读位置失败', { peerId }, e)
}
}
return { send, sendRaw, recall, readActive, syncPrivateReadStatus }
}