feat(im): 统一消息读位置和回执状态模型
- 新增 im_conversation_read 会话读位置表,并补充消息存储推拉相关索引 - 群消息固化 receiver_user_ids 快照,按可见成员快照拉取和统计回执 - 统一消息 status 为 NORMAL/RECALL,新增私聊 receipt_status 并复用统一回执状态 - 前端改用 receiptStatus 展示私聊已读、群回执和频道已读态 - 补齐私聊、群聊、频道 WebSocket 已读同步和离线补偿逻辑 - 更新 IM 消息状态、回执状态字典和管理后台展示 - 调整相关单测和测试建表脚本pull/884/MERGE
parent
8c796950f9
commit
cf85fd4c86
|
|
@ -13,7 +13,7 @@ export interface ImManagerGroupMessageVO {
|
|||
atUserIds?: number[]
|
||||
// 与 atUserIds 同长度;后端对找不到 / 已删除的成员返回 null,UI 用 `?.[idx] || userId` 回退到 userId 渲染
|
||||
atUserNicknames?: (string | null)[]
|
||||
receiptStatus?: number
|
||||
receiptStatus: number
|
||||
sendTime: Date
|
||||
createTime: Date
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export interface ImManagerPrivateMessageVO {
|
|||
type: number
|
||||
content: string
|
||||
status: number
|
||||
receiptStatus: number
|
||||
sendTime: Date
|
||||
createTime: Date
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ export interface ImChannelMessageRespVO {
|
|||
materialId: number
|
||||
type: number
|
||||
content: string
|
||||
/** 当前用户已读态;pull 时按 Redis 游标计算填充,多端同步使用 */
|
||||
status?: number
|
||||
receiptStatus?: number
|
||||
sendTime: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ export interface ImPrivateMessageRespVO {
|
|||
receiverId: number // 接收人编号
|
||||
type: number // 消息类型
|
||||
content: string // 消息内容(JSON 格式)
|
||||
status: number // 消息状态
|
||||
status: number // 消息状态(正常 / 已撤回)
|
||||
receiptStatus?: number // 回执状态(不需要 / 待完成 / 已完成),对齐 ImMessageReceiptStatus
|
||||
sendTime: string // 发送时间
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ export interface ImPrivateMessageSendReqVO {
|
|||
receiverId: number // 接收人编号
|
||||
type: number // 消息类型
|
||||
content: string // 消息内容(JSON 格式)
|
||||
receipt?: boolean // 是否需要回执;不传后端默认 true(普通私聊用户消息)
|
||||
}
|
||||
|
||||
// 私聊历史消息列表 Request VO
|
||||
|
|
|
|||
|
|
@ -329,9 +329,8 @@ export enum DICT_TYPE {
|
|||
|
||||
// ========== IM - 即时通讯模块 ==========
|
||||
IM_MESSAGE_TYPE = 'im_message_type', // IM 消息类型
|
||||
IM_PRIVATE_MESSAGE_STATUS = 'im_private_message_status', // IM 私聊消息状态:0=未读 / 2=已撤回 / 3=已读
|
||||
IM_GROUP_MESSAGE_STATUS = 'im_group_message_status', // IM 群聊消息状态:0=正常 / 2=已撤回
|
||||
IM_GROUP_MESSAGE_RECEIPT_STATUS = 'im_group_message_receipt_status', // IM 群消息回执状态
|
||||
IM_MESSAGE_STATUS = 'im_message_status', // IM 消息状态:0=正常 / 2=已撤回(私聊 / 群聊共用)
|
||||
IM_MESSAGE_RECEIPT_STATUS = 'im_message_receipt_status', // IM 消息回执状态:0=不需要 / 1=待完成 / 2=已完成
|
||||
IM_FRIEND_STATUS = 'im_friend_status', // IM 好友状态
|
||||
IM_FRIEND_ADD_SOURCE = 'im_friend_add_source', // IM 好友添加来源
|
||||
IM_FRIEND_REQUEST_HANDLE_RESULT = 'im_friend_request_handle_result', // IM 好友申请处理结果
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export const useMessagePuller = () => {
|
|||
type: message.type,
|
||||
content: message.content,
|
||||
status: message.status,
|
||||
receiptStatus: message.receiptStatus,
|
||||
sendTime: new Date(message.sendTime).getTime(),
|
||||
senderId: message.senderId,
|
||||
targetId: getPrivatePeerId(message),
|
||||
|
|
@ -109,7 +110,8 @@ export const useMessagePuller = () => {
|
|||
clientMessageId: message.clientMessageId || generateClientMessageId(),
|
||||
type: message.type,
|
||||
content: message.content,
|
||||
status: message.status ?? ImMessageStatus.UNREAD,
|
||||
status: ImMessageStatus.NORMAL, // 频道无撤回,恒为正常
|
||||
receiptStatus: message.receiptStatus, // 频道已读态:DONE 已读 / PENDING 未读
|
||||
sendTime: new Date(message.sendTime).getTime(),
|
||||
senderId: 0, // 系统下发,无发送人
|
||||
targetId: message.channelId, // 会话归属到频道编号
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ interface SendExtOptions {
|
|||
*
|
||||
* 设计要点:
|
||||
* 1. 私聊 / 群聊接口签名对称,按 conversation.type 分支调度,差异在分支内部消化
|
||||
* 2. 发送走「乐观更新」:先 insertMessage 写入 SENDING 占位,请求成功 ackMessage 更新为 UNREAD,失败更新为 FAILED
|
||||
* 2. 发送走「乐观更新」:先 insertMessage 写入 SENDING 占位,请求成功 ackMessage 更新为 NORMAL,失败更新为 FAILED
|
||||
* 3. 撤回不做乐观更新:服务端通过 WebSocket RECALL 事件回传,由 websocketStore 统一更新状态,避免网络失败后不可回退
|
||||
* 4. 已读上报:本端立刻清未读数;服务端回包成功后再做持久化
|
||||
*/
|
||||
|
|
@ -134,7 +134,7 @@ export const useMessageSender = () => {
|
|||
messageStore.insertMessage(conversationInfo, message)
|
||||
}
|
||||
|
||||
// 3. 发送请求:按会话类型分发到不同接口;成功后 ackMessage 更新为 UNREAD,失败更新为 FAILED
|
||||
// 3. 发送请求:按会话类型分发到不同接口;成功后 ackMessage 更新为 NORMAL,失败更新为 FAILED
|
||||
try {
|
||||
if (conversation.type === ImConversationType.PRIVATE) {
|
||||
const data = await apiSendPrivateMessage({
|
||||
|
|
@ -147,6 +147,7 @@ export const useMessageSender = () => {
|
|||
id: data.id,
|
||||
sendTime: new Date(data.sendTime).getTime(),
|
||||
status: data.status,
|
||||
receiptStatus: data.receiptStatus,
|
||||
content: data.content
|
||||
})
|
||||
} else if (conversation.type === ImConversationType.GROUP) {
|
||||
|
|
@ -222,9 +223,8 @@ export const useMessageSender = () => {
|
|||
if (!conversation) {
|
||||
return
|
||||
}
|
||||
// 本地标记已读:未读数清零 + 消息状态更新为 READ(UI 立刻响应)
|
||||
// 本地标记已读:未读数清零(UI 立刻响应)
|
||||
conversationStore.markConversationRead(conversation.type, conversation.targetId)
|
||||
messageStore.markConversationMessageListRead(conversation)
|
||||
const maxMessageId = messageStore
|
||||
.getMessages(getClientConversationId(conversation.type, conversation.targetId))
|
||||
.reduce<number>(
|
||||
|
|
@ -286,7 +286,7 @@ export const useMessageSender = () => {
|
|||
if (!maxReadId) {
|
||||
return
|
||||
}
|
||||
// applyMessageReadReceipt 内部把 ≤ maxReadId 的本端消息更新为 READ
|
||||
// applyMessageReadReceipt 内部把 ≤ maxReadId 的本端消息回执更新为 DONE
|
||||
messageStore.applyMessageReadReceipt({
|
||||
conversationType: ImConversationType.PRIVATE,
|
||||
targetId: peerId,
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
v-else-if="privateReadLabel"
|
||||
class="text-12px whitespace-nowrap"
|
||||
:class="
|
||||
message.status === ImMessageStatus.READ
|
||||
message.receiptStatus === ImMessageReceiptStatus.DONE
|
||||
? 'text-[#409eff]'
|
||||
: 'text-[var(--el-text-color-secondary)]'
|
||||
"
|
||||
|
|
@ -202,7 +202,7 @@ import {
|
|||
ImForwardMode,
|
||||
ImMessageType,
|
||||
ImMessageStatus,
|
||||
ImGroupReceiptStatus,
|
||||
ImMessageReceiptStatus,
|
||||
ImConversationType,
|
||||
ImFriendAddSource,
|
||||
ImGroupMemberRole,
|
||||
|
|
@ -527,10 +527,10 @@ const privateReadLabel = computed(() => {
|
|||
if (conversationStore.activeConversation?.type !== ImConversationType.PRIVATE) {
|
||||
return ''
|
||||
}
|
||||
if (props.message.status === ImMessageStatus.READ) {
|
||||
if (props.message.receiptStatus === ImMessageReceiptStatus.DONE) {
|
||||
return '已读'
|
||||
}
|
||||
if (props.message.status === ImMessageStatus.UNREAD) {
|
||||
if (props.message.receiptStatus === ImMessageReceiptStatus.PENDING) {
|
||||
return '未读'
|
||||
}
|
||||
return ''
|
||||
|
|
@ -551,7 +551,7 @@ const showGroupReadStatus = computed(() => {
|
|||
if (status === undefined || status === null) {
|
||||
return false
|
||||
}
|
||||
return status !== ImGroupReceiptStatus.NO_RECEIPT
|
||||
return status !== ImMessageReceiptStatus.NO_RECEIPT
|
||||
})
|
||||
|
||||
/** 当前群成员(供 MessageReadStatus 计算未读名单;未加载完时兜底空数组不渲染) */
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ import { computed, ref } from 'vue'
|
|||
|
||||
import { getGroupReadUsers as apiGetGroupReadUsers } from '@/api/im/message/group'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { ImConversationType, ImGroupReceiptStatus } from '../../../../../utils/constants'
|
||||
import { ImConversationType, ImMessageReceiptStatus } from '../../../../../utils/constants'
|
||||
import type { Message } from '../../../../types'
|
||||
import { useMessageStore } from '../../../../store/messageStore'
|
||||
import GroupMember, { type GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
||||
|
|
@ -88,7 +88,7 @@ const readUserIds = ref<number[]>([])
|
|||
* - 其他(readCount = 0 或 undefined,且未到 DONE):显示"未读"
|
||||
*/
|
||||
const label = computed(() => {
|
||||
if (props.message.receiptStatus === ImGroupReceiptStatus.DONE) {
|
||||
if (props.message.receiptStatus === ImMessageReceiptStatus.DONE) {
|
||||
return '全部已读'
|
||||
}
|
||||
const readCount = props.message.readCount || 0
|
||||
|
|
@ -147,15 +147,15 @@ async function loadReadUsers() {
|
|||
})
|
||||
readUserIds.value = userIds || []
|
||||
const readCount = readUserIds.value.length
|
||||
// 全可见成员都已读 → flip 到 DONE,让外面 label 直接命中"全部已读"分支;
|
||||
// 否则只更新 readCount,receiptStatus 维持不变(PENDING / READING)
|
||||
// 全可见成员都已读 → 更新为 DONE,让外面 label 直接命中「全部已读」分支;
|
||||
// 否则只更新 readCount,receiptStatus 维持不变(PENDING)
|
||||
const allRead = readCount > 0 && readCount >= visibleMembers.value.length
|
||||
messageStore.applyMessageReadReceipt({
|
||||
conversationType: ImConversationType.GROUP,
|
||||
targetId: props.groupId,
|
||||
groupMessageId: props.message.id,
|
||||
readCount,
|
||||
receiptStatus: allRead ? ImGroupReceiptStatus.DONE : undefined
|
||||
receiptStatus: allRead ? ImMessageReceiptStatus.DONE : undefined
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[IM] 拉取群已读列表失败:', error)
|
||||
|
|
|
|||
|
|
@ -391,9 +391,9 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
this.saveGroup(merged)
|
||||
},
|
||||
|
||||
/** 本地移除(由 WebSocket GROUP_DEL 事件触发) */
|
||||
/** 本地移除群缓存和群会话;群解散(GROUP_DEL)、退群、被踢都复用 */
|
||||
removeGroup(id: number) {
|
||||
// 群解散是硬删(区别于好友删除的软删保留记录);级联清群聊会话避免列表里留死群
|
||||
// 本地硬删(区别于好友删除的软删保留记录);级联清群聊会话避免列表里留死群
|
||||
this.groups = this.groups.filter((g) => g.id !== id)
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.removeGroupConversation(id)
|
||||
|
|
@ -735,7 +735,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
senderId: message.senderId,
|
||||
type: message.type,
|
||||
content: message.content,
|
||||
status: ImMessageStatus.UNREAD,
|
||||
status: ImMessageStatus.NORMAL,
|
||||
sendTime: new Date(message.sendTime).getTime(),
|
||||
targetId: message.groupId || groupId,
|
||||
selfSend: message.senderId === getCurrentUserId(),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { store } from '@/store'
|
|||
import {
|
||||
IM_AT_ALL_USER_ID,
|
||||
ImConversationType,
|
||||
ImMessageReceiptStatus,
|
||||
ImMessageStatus,
|
||||
ImMessageType,
|
||||
isGroupNotification,
|
||||
|
|
@ -194,8 +195,7 @@ function syncConversationAtFlags(conversation: Conversation, message: Message):
|
|||
message.selfSend ||
|
||||
conversation.type !== ImConversationType.GROUP ||
|
||||
!message.atUserIds ||
|
||||
message.atUserIds.length === 0 ||
|
||||
message.status === ImMessageStatus.READ
|
||||
message.atUserIds.length === 0
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
@ -518,7 +518,6 @@ export const useMessageStore = defineStore('imMessageStore', {
|
|||
!message.selfSend &&
|
||||
!isActive &&
|
||||
isNormalMessage(message.type) &&
|
||||
message.status !== ImMessageStatus.READ &&
|
||||
message.status !== ImMessageStatus.RECALL
|
||||
) {
|
||||
conversation.unreadCount++
|
||||
|
|
@ -616,7 +615,6 @@ export const useMessageStore = defineStore('imMessageStore', {
|
|||
!message.selfSend &&
|
||||
!isActive &&
|
||||
isNormalMessage(message.type) &&
|
||||
message.status !== ImMessageStatus.READ &&
|
||||
message.status !== ImMessageStatus.RECALL
|
||||
) {
|
||||
conversation.unreadCount++
|
||||
|
|
@ -779,9 +777,9 @@ export const useMessageStore = defineStore('imMessageStore', {
|
|||
message.selfSend &&
|
||||
message.id &&
|
||||
message.id <= options.privateReadMaxId! &&
|
||||
message.status !== ImMessageStatus.RECALL
|
||||
message.receiptStatus === ImMessageReceiptStatus.PENDING
|
||||
) {
|
||||
message.status = ImMessageStatus.READ
|
||||
message.receiptStatus = ImMessageReceiptStatus.DONE
|
||||
changed.push(message)
|
||||
}
|
||||
})
|
||||
|
|
@ -871,28 +869,6 @@ export const useMessageStore = defineStore('imMessageStore', {
|
|||
conversationStore.saveConversation(conversation)
|
||||
},
|
||||
|
||||
/** 当前会话标记已读 */
|
||||
markConversationMessageListRead(conversation: Conversation) {
|
||||
const messages = this.getMessageList(conversation.type, conversation.targetId)
|
||||
const changed: Message[] = []
|
||||
messages.forEach((message) => {
|
||||
if (!message.selfSend && message.status === ImMessageStatus.UNREAD) {
|
||||
message.status = ImMessageStatus.READ
|
||||
changed.push(message)
|
||||
}
|
||||
})
|
||||
if (changed.length === 0) {
|
||||
return
|
||||
}
|
||||
void getDb()
|
||||
.transaction(['messages'], 'readwrite', async (tx) => {
|
||||
for (const message of changed) {
|
||||
await this.saveMessageRecord(message, conversation.type, tx)
|
||||
}
|
||||
})
|
||||
.catch((e) => console.warn('[IM messageStore] 已读状态写入失败', e))
|
||||
},
|
||||
|
||||
/** 删除会话全部消息 */
|
||||
deleteConversationMessageList(conversationType: number, targetId: number) {
|
||||
// 1. 清理内存消息和媒体资源
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { getCurrentUserId, getRefreshToken } from '@/utils/auth'
|
|||
import {
|
||||
ImWebSocketMessageType,
|
||||
ImMessageStatus,
|
||||
ImMessageReceiptStatus,
|
||||
ImMessageType,
|
||||
ImConversationType,
|
||||
ImRtcCallMediaType,
|
||||
|
|
@ -41,6 +42,7 @@ import {
|
|||
} from './rtcStore'
|
||||
import { readPrivateMessages as apiReadPrivateMessages } from '@/api/im/message/private'
|
||||
import { readGroupMessages as apiReadGroupMessages } from '@/api/im/message/group'
|
||||
import { readChannelMessages as apiReadChannelMessages } from '@/api/im/message/channel'
|
||||
import type { ImChannelMessageRespVO } from '@/api/im/message/channel'
|
||||
import { buildChannelConversationStub } from '../../utils/channel'
|
||||
import type {
|
||||
|
|
@ -106,6 +108,7 @@ const convertPrivateMessage = (
|
|||
type: websocketMessage.type,
|
||||
content: websocketMessage.content,
|
||||
status: websocketMessage.status,
|
||||
receiptStatus: websocketMessage.receiptStatus,
|
||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||
senderId: websocketMessage.senderId,
|
||||
targetId: getPrivateMessagePeerId(websocketMessage, currentUserId),
|
||||
|
|
@ -334,19 +337,6 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
typeof websocketMessage.sendTime === 'number'
|
||||
? websocketMessage.sendTime
|
||||
: new Date(websocketMessage.sendTime).getTime()
|
||||
messageStore.insertMessage(buildChannelConversationStub(websocketMessage.channelId), {
|
||||
id: websocketMessage.id,
|
||||
clientMessageId: '',
|
||||
type: websocketMessage.type,
|
||||
content: websocketMessage.content,
|
||||
status: ImMessageStatus.UNREAD,
|
||||
sendTime: sendTimeMs,
|
||||
senderId: 0,
|
||||
targetId: websocketMessage.channelId,
|
||||
selfSend: false,
|
||||
materialId: websocketMessage.materialId
|
||||
})
|
||||
// 非当前会话 + 未免打扰:响一下提示音
|
||||
const conversation = conversationStore.getConversation(
|
||||
ImConversationType.CHANNEL,
|
||||
websocketMessage.channelId
|
||||
|
|
@ -354,7 +344,28 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
const isActive =
|
||||
conversationStore.activeConversation?.type === ImConversationType.CHANNEL &&
|
||||
conversationStore.activeConversation?.targetId === websocketMessage.channelId
|
||||
if (!isActive && !conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
||||
// 频道单向订阅,receiptStatus 表达「我是否已读这条」:会话打开即已读 DONE,否则 PENDING(与 pull 口径一致)
|
||||
messageStore.insertMessage(buildChannelConversationStub(websocketMessage.channelId), {
|
||||
id: websocketMessage.id,
|
||||
clientMessageId: '',
|
||||
type: websocketMessage.type,
|
||||
content: websocketMessage.content,
|
||||
status: ImMessageStatus.NORMAL,
|
||||
receiptStatus: isActive ? ImMessageReceiptStatus.DONE : ImMessageReceiptStatus.PENDING,
|
||||
sendTime: sendTimeMs,
|
||||
senderId: 0,
|
||||
targetId: websocketMessage.channelId,
|
||||
selfSend: false,
|
||||
materialId: websocketMessage.materialId
|
||||
})
|
||||
if (isActive) {
|
||||
// 窗口打开 = 已读:本端清未读 + 上报服务端读位置,避免读位置滞后
|
||||
conversationStore.markConversationRead(ImConversationType.CHANNEL, websocketMessage.channelId)
|
||||
apiReadChannelMessages(websocketMessage.channelId, websocketMessage.id).catch((e) => {
|
||||
console.warn('[IM WS] 频道自动已读上报失败', e)
|
||||
})
|
||||
} else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
|
||||
// 非当前会话且未免打扰:响一下提示音
|
||||
playAudioTip()
|
||||
}
|
||||
},
|
||||
|
|
@ -543,9 +554,6 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
// 聊天窗口打开 = 实际看到了:本端清未读;私聊已读开启时再上报后端,让对方 UI 立刻切到"已读"
|
||||
// 已读位置直接用刚到的消息 id(这条就是当前会话最大 id)
|
||||
conversationStore.markConversationRead(ImConversationType.PRIVATE, peerId)
|
||||
if (conversation) {
|
||||
useMessageStore().markConversationMessageListRead(conversation)
|
||||
}
|
||||
if (MESSAGE_PRIVATE_READ_ENABLED) {
|
||||
apiReadPrivateMessages(peerId, websocketMessage.id).catch((e) => {
|
||||
console.warn('[IM WS] 自动已读上报失败', e)
|
||||
|
|
@ -673,9 +681,6 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
ImConversationType.GROUP,
|
||||
websocketMessage.groupId
|
||||
)
|
||||
if (conversation) {
|
||||
useMessageStore().markConversationMessageListRead(conversation)
|
||||
}
|
||||
if (MESSAGE_GROUP_READ_ENABLED) {
|
||||
apiReadGroupMessages(websocketMessage.groupId, websocketMessage.id).catch((e) => {
|
||||
console.warn('[IM WS] 自动已读上报失败', e)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface ImPrivateMessageDTO {
|
|||
type: number // 消息类型
|
||||
content: string // 消息内容
|
||||
status: number // 消息状态
|
||||
receiptStatus?: number // 回执状态(不需要 / 待完成 / 已完成)
|
||||
sendTime: string // 发送时间
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +94,7 @@ export interface Message {
|
|||
senderId: number // 发送人编号
|
||||
atUserIds?: number[] // 群 @ 目标用户列表
|
||||
receiverUserIds?: number[] // 群定向接收用户列表
|
||||
receiptStatus?: number // 群回执状态,对齐 ImGroupReceiptStatus(仅群消息)
|
||||
receiptStatus?: number // 回执状态,对齐 ImMessageReceiptStatus(私聊 / 群 / 频道通用)
|
||||
readCount?: number // 群回执已读人数(仅群消息)
|
||||
materialId?: number // 关联频道素材编号(仅频道消息 type=MATERIAL)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@
|
|||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_TYPE" :value="detail.type" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="MESSAGE_GROUP_READ_ENABLED" label="状态">
|
||||
<dict-tag :type="DICT_TYPE.IM_GROUP_MESSAGE_STATUS" :value="detail.status" />
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_STATUS" :value="detail.status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="MESSAGE_GROUP_READ_ENABLED" label="回执">
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_RECEIPT_STATUS" :value="detail.receiptStatus" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="@用户" :span="2">
|
||||
<template v-if="detail.atUserIds?.length">
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
width="110"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_GROUP_MESSAGE_RECEIPT_STATUS" :value="row.receiptStatus" />
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_RECEIPT_STATUS" :value="row.receiptStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
|
|
@ -131,7 +131,7 @@
|
|||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_GROUP_MESSAGE_STATUS" :value="row.status" />
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_STATUS" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="100" fixed="right">
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@
|
|||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_TYPE" :value="detail.type" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="MESSAGE_PRIVATE_READ_ENABLED" label="状态">
|
||||
<dict-tag :type="DICT_TYPE.IM_PRIVATE_MESSAGE_STATUS" :value="detail.status" />
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_STATUS" :value="detail.status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="MESSAGE_PRIVATE_READ_ENABLED" label="回执">
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_RECEIPT_STATUS" :value="detail.receiptStatus" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发送时间" :span="2">
|
||||
{{ formatDate(detail.sendTime) }}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,18 @@
|
|||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_PRIVATE_MESSAGE_STATUS" :value="row.status" />
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_STATUS" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="MESSAGE_PRIVATE_READ_ENABLED"
|
||||
label="回执"
|
||||
align="center"
|
||||
prop="receiptStatus"
|
||||
width="110"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_RECEIPT_STATUS" :value="row.receiptStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
|
|
|
|||
|
|
@ -138,14 +138,13 @@ export function isMediaMessageType(type: number): boolean {
|
|||
/**
|
||||
* IM 消息状态枚举(对齐后端 ImMessageStatusEnum,前端扩展 SENDING + FAILED)
|
||||
*
|
||||
* 生命周期:FAILED(-2) → SENDING(-1) → UNREAD(0) → READ(3) / RECALL(2)
|
||||
* 生命周期:FAILED(-2) → SENDING(-1) → NORMAL(0) → RECALL(2)
|
||||
*/
|
||||
export const ImMessageStatus = {
|
||||
FAILED: -2, // 发送失败(前端独有)
|
||||
SENDING: -1, // 发送中(前端独有)
|
||||
UNREAD: 0, // 未读
|
||||
RECALL: 2, // 已撤回
|
||||
READ: 3 // 已读
|
||||
NORMAL: 0, // 正常
|
||||
RECALL: 2 // 已撤回
|
||||
} as const
|
||||
|
||||
/** IM 会话类型枚举 */
|
||||
|
|
@ -233,8 +232,8 @@ export const ImWebSocketMessageType = {
|
|||
CHANNEL_MESSAGE: 'im-channel-message' // 频道通道
|
||||
} as const
|
||||
|
||||
/** IM 群回执状态枚举(对齐后端 ImGroupMessageReceiptStatusEnum) */
|
||||
export const ImGroupReceiptStatus = {
|
||||
/** IM 消息回执状态枚举(对齐后端 ImMessageReceiptStatusEnum) */
|
||||
export const ImMessageReceiptStatus = {
|
||||
NO_RECEIPT: 0, // 不需要回执
|
||||
PENDING: 1, // 待完成
|
||||
DONE: 2 // 已完成
|
||||
|
|
|
|||
Loading…
Reference in New Issue