feat(im): 消息右键菜单优化 + 修复图片场景滚不到底

- MessageItem:「回复」→「引用」并加图标;撤回 / 删除互斥(自己消息 2 分钟内显示撤回,超出 / 对方消息显示删除),均加分割线 + 红色样式对齐微信;MENU_KEYS 抽 const 防 typo;引用块从气泡上方移到下方,selfSend 时竖线镜像到右侧
- MessagePanel:scrollToBottom 改 async + waitMediaSettled 等图片 / 视频元数据加载;用 expectedScrollTop drift 替代 distanceFromBottom,修复「图片加载完底部上移、误判用户已滚走」导致到不了底
- ReplyPreview:删等价的 filePayload / voicePayload alias,直接复用 parsedPayload
- uiStore:ContextMenuItem 加 icon? 字段,支持菜单项前置图标
im
YunaiV 2026-05-01 23:06:14 +08:00
parent 52fdf0bcab
commit fbd8615398
1 changed files with 56 additions and 16 deletions

View File

@ -499,11 +499,21 @@ const isAtMe = computed(() => {
return (props.message.atUserIds || []).includes(myId)
})
/** 右键菜单 key 常量push 端和分发端从同一处取typo 编译期就能抓 */
const MENU_KEYS = {
REPLY: 'REPLY',
RECALL: 'RECALL',
DELETE: 'DELETE'
} as const
type MenuKey = (typeof MENU_KEYS)[keyof typeof MENU_KEYS]
/** 撤回时间窗:自己发送的消息超过这个时长就不能再撤回,菜单回退为「删除」(对齐微信 2 分钟) */
const RECALL_WINDOW_MS = 2 * 60 * 1000
/**
* 右键菜单项
* - 回复仅已落库id0且未撤回的消息可引用引用块写入 draftStore.reply
* - 删除从本地消息列表移除不动后端
* - 撤回仅自己发送已送达 id的消息
* - 引用已落库id0+ 未撤回的消息可引用引用块写入 draftStore.reply
* - 撤回 / 删除互斥自己发送 + 已落库 + 未撤回 + 2 分钟内显示撤回推服务器其它显示删除仅本地清
*
* TIP_TIME / TIP_TEXT 态不弹菜单
*/
@ -512,26 +522,56 @@ async function handleContextMenu(e: MouseEvent) {
return
}
const items: Array<{ key: string; name: string; disabled?: boolean }> = []
// "" (id0) + ;, id
// TODO @AI
const items: Array<{
key: MenuKey
name: string
disabled?: boolean
divided?: boolean
danger?: boolean
icon?: string
}> = []
// id0+ id=0 id
if (!!props.message.id && !isRecall.value) {
items.push({ key: 'REPLY', name: '回复' })
items.push({
key: MENU_KEYS.REPLY,
name: '引用',
icon: 'bxs:quote-alt-left'
})
}
// TODO @AI
if (props.message.selfSend && !!props.message.id && !isRecall.value) {
items.push({ key: 'RECALL', name: '撤回' })
// /
// - + id0+ + RECALL
// - / /
// divided danger
const canRecall =
props.message.selfSend &&
!!props.message.id &&
!isRecall.value &&
Date.now() - props.message.sendTime <= RECALL_WINDOW_MS
if (canRecall) {
items.push({
key: MENU_KEYS.RECALL,
name: '撤回',
icon: 'ant-design:undo-outlined',
divided: true,
danger: true
})
} else {
items.push({
key: MENU_KEYS.DELETE,
name: '删除',
icon: 'ant-design:delete-outlined',
divided: true,
danger: true
})
}
// """" + id0+
// TODO @AI --- 线
items.push({ key: 'DELETE', name: '删除' })
// uiStore DOMcallback key
uiStore.openContextMenu({ x: e.clientX, y: e.clientY }, items, async (item) => {
if (item.key === 'REPLY') {
if (item.key === MENU_KEYS.REPLY) {
handleReply()
} else if (item.key === 'RECALL') {
} else if (item.key === MENU_KEYS.RECALL) {
await handleRecall()
} else if (item.key === 'DELETE') {
} else if (item.key === MENU_KEYS.DELETE) {
handleDelete()
}
})