✨ feat(im): 优化名片消息类型 v0.4:增加转发成功失败的提示
parent
1ac0650984
commit
4868d69ed8
|
|
@ -293,7 +293,12 @@ function buildCardContent(user: User): string {
|
|||
return serializeMessage(payload)
|
||||
}
|
||||
|
||||
/** 确认发送:每个选中会话先发 CARD 再发 TEXT 留言;失败的消息会以 FAILED 状态留在对应会话气泡里供右键重试 */
|
||||
/**
|
||||
* 确认发送:每个选中会话先发 CARD,CARD 成功后才发留言(保证「先看到名片」的顺序意图,CARD 失败时不发留言避免错序)
|
||||
*
|
||||
* 文案聚合:全部成功「已转发」、全部失败「转发失败:A、B」、部分失败「已转发,但 X、Y 失败」(具体列出失败会话名方便定位);
|
||||
* 失败的消息以 FAILED 状态留在对应会话气泡里,可右键重试
|
||||
*/
|
||||
async function handleSend() {
|
||||
const user = props.user
|
||||
if (!user?.id || selectedKeys.value.length === 0) {
|
||||
|
|
@ -305,13 +310,22 @@ async function handleSend() {
|
|||
sending.value = true
|
||||
try {
|
||||
const tasks = targets.map(async (target) => {
|
||||
await sendRaw(ImMessageType.CARD, cardContent, { conversation: target })
|
||||
if (leaveText) {
|
||||
await send(leaveText, { conversation: target })
|
||||
const cardOk = await sendRaw(ImMessageType.CARD, cardContent, { conversation: target })
|
||||
if (!cardOk) {
|
||||
return { target, ok: false }
|
||||
}
|
||||
const ok = leaveText ? await send(leaveText, { conversation: target }) : true
|
||||
return { target, ok }
|
||||
})
|
||||
await Promise.all(tasks)
|
||||
message.success('已转发')
|
||||
const results = await Promise.all(tasks)
|
||||
const failedNames = results.filter((r) => !r.ok).map((r) => r.target.name || '未命名会话')
|
||||
if (failedNames.length === 0) {
|
||||
message.success('已转发')
|
||||
} else if (failedNames.length === targets.length) {
|
||||
message.error(`转发失败:${failedNames.join('、')}`)
|
||||
} else {
|
||||
message.warning(`已转发,但 ${failedNames.join('、')} 失败`)
|
||||
}
|
||||
visible.value = false
|
||||
} finally {
|
||||
sending.value = false
|
||||
|
|
|
|||
|
|
@ -83,16 +83,22 @@ export const useMessageSender = () => {
|
|||
* 发送任意类型的消息(底层实现)
|
||||
* 1. 文本、图片、文件、语音等都走这里
|
||||
* 2. type / content 由调用方构造
|
||||
* 3. 返回值:成功 true / 失败 false(失败时本地占位已标 FAILED);参数缺失等无法发送的场景也返 false
|
||||
* 转发 / 名片推荐等场景按返回值决定是否继续后续动作(如有留言时仅在名片成功后再发留言)
|
||||
*/
|
||||
const sendRaw = async (type: number, content: string, options?: SendExtOptions) => {
|
||||
const sendRaw = async (
|
||||
type: number,
|
||||
content: string,
|
||||
options?: SendExtOptions
|
||||
): Promise<boolean> => {
|
||||
// 1. 参数校验:优先用显式传入的 conversation(转发场景),否则取激活会话
|
||||
const conversation = options?.conversation ?? conversationStore.activeConversation
|
||||
if (!conversation) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
const realTarget = options?.targetId || conversation.targetId
|
||||
if (!realTarget) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 准备 clientMessageId:媒体上传链路在 step 1 已经 insertMessage 占位,这里直接复用 id;其余场景走默认乐观插入
|
||||
|
|
@ -106,7 +112,7 @@ export const useMessageSender = () => {
|
|||
(m) => m.clientMessageId === clientMessageId
|
||||
)
|
||||
if (!stillExists) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
clientMessageId = generateClientMessageId()
|
||||
|
|
@ -159,21 +165,26 @@ export const useMessageSender = () => {
|
|||
content: data.content
|
||||
})
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error('[IM] 消息发送失败', { type, realTarget, clientMessageId }, e)
|
||||
conversationStore.ackMessage(conversation.type, realTarget, clientMessageId, {
|
||||
status: ImMessageStatus.FAILED
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送文本消息(最常用的快捷入口):MessageInput.vue 文本回车走这里 */
|
||||
const send = async (text: string, options?: SendExtOptions) => {
|
||||
/**
|
||||
* 发送文本消息(最常用的快捷入口):MessageInput.vue 文本回车走这里
|
||||
* 返回值:成功 true / 失败 false / 空文本 false(与 sendRaw 对齐,转发场景按返回值判断)
|
||||
*/
|
||||
const send = async (text: string, options?: SendExtOptions): Promise<boolean> => {
|
||||
if (!text.trim()) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
const payload = withQuotePayload<TextMessage>({ content: text }, options?.quote)
|
||||
await sendRaw(ImMessageType.TEXT, serializeMessage(payload), options)
|
||||
return sendRaw(ImMessageType.TEXT, serializeMessage(payload), options)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -69,7 +69,17 @@ export function isFriendChatTip(type: number): boolean {
|
|||
return type === ImMessageType.FRIEND_ADD || type === ImMessageType.FRIEND_DELETE
|
||||
}
|
||||
|
||||
/** IM 普通消息类型集合(聊天气泡中显示,并作为会话最后一条摘要) */
|
||||
/**
|
||||
* IM 普通消息类型集合(normal vs event 二分;与后端 ImMessageTypeEnum.normal 字段语义一致)
|
||||
*
|
||||
* 这个集合在多处被复用,新增类型前先确认所有副作用都符合预期:
|
||||
* 1. 后端发送入口校验(Im{Private,Group}MessageSendReqVO.isNormalType)—— 用户发送的消息类型必须 normal=true
|
||||
* 2. 前端接收侧未读 / 提示音(websocketStore)—— normal 消息计入会话未读数 + 触发声音
|
||||
* 3. 前端会话列表 lastType / @ 标签(ConversationItem)—— 只有 normal 才算「最后一条聊天消息」
|
||||
* 4. 前端群消息置顶菜单(MessageItem.vue 的 canPin)—— normal 才允许群主 / 管理员置顶
|
||||
*
|
||||
* 名片(CARD)属于「用户主动发的聊天消息」,1/2/3 都符合预期;4 同时被放开 = 群主可置顶名片,语义合理
|
||||
*/
|
||||
const ImMessageTypeNormals: number[] = [
|
||||
ImMessageType.TEXT,
|
||||
ImMessageType.IMAGE,
|
||||
|
|
|
|||
Loading…
Reference in New Issue