refactor(im): 移除 TIP_TIME 消息类型,时间分隔条改为渲染时按 prevMessage.sendTime 计算
顺带修复 Bug-Y(删除最后一条消息后孤立时间分隔条)im
parent
63c4dd1096
commit
1b51926b19
|
|
@ -127,7 +127,7 @@ const lastSenderDisplayName = computed(() => {
|
|||
)
|
||||
})
|
||||
|
||||
/** 群聊 + 有最后发送者 + 最后一条是普通消息时,显示发送者前缀(TIP_TIME / TIP_TEXT / RECALL / 草稿态不带前缀) */
|
||||
/** 群聊 + 有最后发送者 + 最后一条是普通消息时,显示发送者前缀(TIP_TEXT / RECALL / 草稿态不带前缀) */
|
||||
const showSendName = computed(() => {
|
||||
if (draft.value) {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -529,16 +529,13 @@ function matchesActiveFilter(message: Message): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* 当前列表:先剔除 TIP_TIME(每行已有绝对时间,时间分隔线无意义)、
|
||||
* 再 activeFilter 过滤、再 keyword 模糊命中、最后 reverse(最新在前)
|
||||
* 当前列表:先 activeFilter 过滤、再 keyword 模糊命中、最后 reverse(最新在前)
|
||||
*
|
||||
* 关键字命中走 textSnippetOf —— 文本拿原文、媒体拿"[图片]"等占位词、文件拿文件名
|
||||
*/
|
||||
const currentList = computed<Message[]>(() => {
|
||||
const trimmedKeyword = keyword.value.trim()
|
||||
let list = allMessages.value
|
||||
.filter((message) => message.type !== ImMessageType.TIP_TIME)
|
||||
.filter(matchesActiveFilter)
|
||||
let list = allMessages.value.filter(matchesActiveFilter)
|
||||
if (trimmedKeyword) {
|
||||
list = list.filter((message) => textSnippetOf(message).includes(trimmedKeyword))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<!-- 时间分隔线(TIP_TIME=20):居中灰色时间 -->
|
||||
<!-- 时间分隔条:列表第一条 / 距上一条超过阈值时居中显示灰色时间 -->
|
||||
<div
|
||||
v-if="isTipTime"
|
||||
class="flex items-center justify-center px-4 pt-2.5 pb-0.5 text-12px text-[var(--el-text-color-disabled)]"
|
||||
v-if="shouldShowTimeTip"
|
||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-disabled)]"
|
||||
>
|
||||
{{ formatTipTime(message.sendTime) }}
|
||||
</div>
|
||||
|
||||
<!-- 系统提示文案(TIP_TEXT=21) -->
|
||||
<div
|
||||
v-else-if="isTipText"
|
||||
v-if="isTipText"
|
||||
class="flex items-center justify-center px-4 py-2 text-12px text-[var(--el-text-color-secondary)]"
|
||||
>
|
||||
{{ tipText }}
|
||||
|
|
@ -226,6 +226,7 @@ import {
|
|||
ImGroupReceiptStatus,
|
||||
ImConversationType,
|
||||
ImGroupMemberRole,
|
||||
TIME_TIP_GAP_MS,
|
||||
isGroupNotification,
|
||||
isNormalMessage
|
||||
} from '@/views/im/utils/constants'
|
||||
|
|
@ -268,6 +269,8 @@ defineOptions({ name: 'ImMessageItem' })
|
|||
|
||||
const props = defineProps<{
|
||||
message: Message
|
||||
/** 列表中的上一条消息,用于判断是否要在当前消息上方渲染时间分隔条;不传按列表第一条处理 */
|
||||
prevMessage?: Message
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -292,10 +295,20 @@ const { confirm: confirmDialog, success: successMessage } = useMessage()
|
|||
/** 是否已撤回:pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL,渲染只需识别 type */
|
||||
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
|
||||
|
||||
/** 时间分隔线 / 系统提示文案 */
|
||||
const isTipTime = computed(() => props.message.type === ImMessageType.TIP_TIME)
|
||||
/** 系统提示文案 */
|
||||
const isTipText = computed(() => props.message.type === ImMessageType.TIP_TEXT)
|
||||
|
||||
/** 是否在当前消息上方渲染时间分隔条:列表第一条 / 距上一条超过阈值;缺 sendTime 不渲染 */
|
||||
const shouldShowTimeTip = computed(() => {
|
||||
if (!props.message.sendTime) {
|
||||
return false
|
||||
}
|
||||
if (!props.prevMessage?.sendTime) {
|
||||
return true
|
||||
}
|
||||
return props.message.sendTime - props.prevMessage.sendTime > TIME_TIP_GAP_MS
|
||||
})
|
||||
|
||||
/** 是否文本消息 */
|
||||
const isText = computed(() => props.message.type === ImMessageType.TEXT)
|
||||
const isImage = computed(() => props.message.type === ImMessageType.IMAGE)
|
||||
|
|
@ -540,10 +553,10 @@ const RECALL_WINDOW_MS = 2 * 60 * 1000
|
|||
* - 引用:已落库(id≠0)+ 未撤回的消息可引用,引用块写入 draftStore.reply
|
||||
* - 撤回 / 删除:互斥;自己发送 + 已落库 + 未撤回 + 2 分钟内显示「撤回」(推服务器),其它显示「删除」(仅本地清)
|
||||
*
|
||||
* TIP_TIME / TIP_TEXT 态不弹菜单
|
||||
* TIP_TEXT 态不弹菜单
|
||||
*/
|
||||
async function handleContextMenu(e: MouseEvent) {
|
||||
if (isTipTime.value || isTipText.value) {
|
||||
if (isTipText.value) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,12 +80,16 @@
|
|||
<!-- data-message-id 给 MessageHistory "定位到聊天位置" 用:父级通过 querySelector
|
||||
找到这层 wrapper,scrollIntoView + 加高亮 class;id=0 的本地占位消息跳过 -->
|
||||
<div
|
||||
v-for="msg in messages"
|
||||
v-for="(msg, index) in messages"
|
||||
:key="msg.id || msg.clientMessageId"
|
||||
:data-message-id="msg.id || ''"
|
||||
class="message-panel__message-anchor"
|
||||
>
|
||||
<MessageItem :message="msg" @locate="handleLocate" />
|
||||
<MessageItem
|
||||
:message="msg"
|
||||
:prev-message="messages[index - 1]"
|
||||
@locate="handleLocate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 回到底部浮动按钮(滚动不在底部时显示) -->
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ import {
|
|||
ImMessageType,
|
||||
ImMessageStatus,
|
||||
IM_AT_ALL_USER_ID,
|
||||
TIME_TIP_GAP_MS,
|
||||
isGroupNotification,
|
||||
isNormalMessage
|
||||
} from '../../utils/constants'
|
||||
import { getCurrentUserId, imStorage, removeQuietly, StorageKeys } from '../../utils/storage'
|
||||
import { generateClientMessageId, parseRecallMessageId } from '../../utils/message'
|
||||
import { parseRecallMessageId } from '../../utils/message'
|
||||
import { resolveConversationLastContent } from '../../utils/conversation'
|
||||
import { tryGetSenderDisplayName } from '../../utils/user'
|
||||
import { useGroupStore } from './groupStore'
|
||||
|
|
@ -297,8 +296,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
top: false,
|
||||
muted: false,
|
||||
atMe: false,
|
||||
atAll: false,
|
||||
lastTimeTip: 0
|
||||
atAll: false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -359,7 +357,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
* 主要行为(子步骤见函数内 // x.y 注释):
|
||||
* 1. 会话定位:查找或创建 + 去重合并
|
||||
* 2. 更新会话元数据:摘要、@ 标记、未读数
|
||||
* 3. 插入消息:时间分隔线 + 按 id 有序插入
|
||||
* 3. 按 id 有序插入消息
|
||||
* 4. 收尾:更新游标 + 持久化
|
||||
*/
|
||||
insertMessage(
|
||||
|
|
@ -459,32 +457,12 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
conversation.unreadCount++
|
||||
}
|
||||
|
||||
// 3.1 时间分隔线:距上条 TIP_TIME 超过 10 分钟则插入一条
|
||||
const sendTime = messageInfo.sendTime || Date.now()
|
||||
if (!conversation.lastTimeTip || conversation.lastTimeTip < sendTime - TIME_TIP_GAP_MS) {
|
||||
conversation.messages.push({
|
||||
id: 0,
|
||||
clientMessageId: generateClientMessageId(),
|
||||
type: ImMessageType.TIP_TIME,
|
||||
content: '',
|
||||
status: ImMessageStatus.UNREAD,
|
||||
sendTime,
|
||||
senderId: 0,
|
||||
targetId: conversationInfo.targetId,
|
||||
selfSend: false
|
||||
})
|
||||
conversation.lastTimeTip = sendTime
|
||||
}
|
||||
|
||||
// 3.2 根据 id 插入到正确位置(防止乱序);tip 消息 / 本地临时消息直接追加末尾
|
||||
// 3. 按真实 id 升序插入;id=0 的本地占位(SENDING)固定停在末尾,
|
||||
// 并发场景下:占位仍在末尾时收到的真实消息会追加在占位之后,列表不严格按 id 升序
|
||||
let insertIndex = conversation.messages.length
|
||||
if (messageInfo.id) {
|
||||
for (let index = 0; index < conversation.messages.length; index++) {
|
||||
const existing = conversation.messages[index]
|
||||
// TIP_TIME 没有 id,不参与排序
|
||||
if (existing.type === ImMessageType.TIP_TIME) {
|
||||
continue
|
||||
}
|
||||
if (existing.id && messageInfo.id < existing.id) {
|
||||
insertIndex = index
|
||||
break
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ export interface Conversation {
|
|||
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
atMe?: boolean // 群聊:是否有人 @我
|
||||
atAll?: boolean // 群聊:是否有人 @全体成员
|
||||
lastTimeTip?: number // 最后一条"时间分隔线"的时间戳,判断是否需要插入下一条 TIP_TIME
|
||||
}
|
||||
|
||||
// 消息数据结构
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const ImMessageType = {
|
|||
RECALL: 10, // 撤回
|
||||
READ: 11, // 已读
|
||||
RECEIPT: 12, // 回执
|
||||
TIP_TIME: 20, // 时间分隔线(前端本地生成,不发送到后端)
|
||||
TIP_TEXT: 21, // 提示文本(撤回提示等)
|
||||
// 好友通知(1201-1210 复用 OpenIM 段位编号)
|
||||
FRIEND_REQUEST_APPROVED: 1201, // 好友申请被同意
|
||||
|
|
@ -141,7 +140,7 @@ export const PRIVATE_MESSAGE_PULL_SIZE = 100
|
|||
/** 每次拉取群聊消息的最大条数(后端上限 1000,前端取保守值 100) */
|
||||
export const GROUP_MESSAGE_PULL_SIZE = 100
|
||||
|
||||
/** 会话之间插入"时间分隔线"的阈值:10 分钟 */
|
||||
/** 消息之间渲染「时间分隔条」的阈值:10 分钟 */
|
||||
export const TIME_TIP_GAP_MS = 10 * 60 * 1000
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue