refactor(im): 移除 TIP_TIME 消息类型,时间分隔条改为渲染时按 prevMessage.sendTime 计算

顺带修复 Bug-Y(删除最后一条消息后孤立时间分隔条)
im
YunaiV 2026-05-04 16:05:23 +08:00
parent 63c4dd1096
commit 1b51926b19
7 changed files with 36 additions and 46 deletions

View File

@ -127,7 +127,7 @@ const lastSenderDisplayName = computed(() => {
)
})
/** 群聊 + 有最后发送者 + 最后一条是普通消息时显示发送者前缀TIP_TIME / TIP_TEXT / RECALL / 草稿态不带前缀) */
/** 群聊 + 有最后发送者 + 最后一条是普通消息时显示发送者前缀TIP_TEXT / RECALL / 草稿态不带前缀) */
const showSendName = computed(() => {
if (draft.value) {
return false

View File

@ -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))
}

View File

@ -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
* - 引用已落库id0+ 未撤回的消息可引用引用块写入 draftStore.reply
* - 撤回 / 删除互斥自己发送 + 已落库 + 未撤回 + 2 分钟内显示撤回推服务器其它显示删除仅本地清
*
* TIP_TIME / TIP_TEXT 态不弹菜单
* TIP_TEXT 态不弹菜单
*/
async function handleContextMenu(e: MouseEvent) {
if (isTipTime.value || isTipText.value) {
if (isTipText.value) {
return
}

View File

@ -80,12 +80,16 @@
<!-- data-message-id MessageHistory "定位到聊天位置" 父级通过 querySelector
找到这层 wrapperscrollIntoView + 加高亮 classid=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>
<!-- 回到底部浮动按钮滚动不在底部时显示 -->

View File

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

View File

@ -62,7 +62,6 @@ export interface Conversation {
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
atMe?: boolean // 群聊:是否有人 @我
atAll?: boolean // 群聊:是否有人 @全体成员
lastTimeTip?: number // 最后一条"时间分隔线"的时间戳,判断是否需要插入下一条 TIP_TIME
}
// 消息数据结构

View File

@ -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
/**