From fbd8615398816f016b1e83befd48e740e5abdba0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 1 May 2026 23:06:14 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95=E4=BC=98=E5=8C=96=20+=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BE=E7=89=87=E5=9C=BA=E6=99=AF=E6=BB=9A?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=E5=BA=95=20-=20MessageItem=EF=BC=9A=E3=80=8C?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E3=80=8D=E2=86=92=E3=80=8C=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E3=80=8D=E5=B9=B6=E5=8A=A0=E5=9B=BE=E6=A0=87=EF=BC=9B=E6=92=A4?= =?UTF-8?q?=E5=9B=9E=20/=20=E5=88=A0=E9=99=A4=E4=BA=92=E6=96=A5=EF=BC=88?= =?UTF-8?q?=E8=87=AA=E5=B7=B1=E6=B6=88=E6=81=AF=202=20=E5=88=86=E9=92=9F?= =?UTF-8?q?=E5=86=85=E6=98=BE=E7=A4=BA=E6=92=A4=E5=9B=9E=EF=BC=8C=E8=B6=85?= =?UTF-8?q?=E5=87=BA=20/=20=E5=AF=B9=E6=96=B9=E6=B6=88=E6=81=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=88=A0=E9=99=A4=EF=BC=89=EF=BC=8C=E5=9D=87=E5=8A=A0?= =?UTF-8?q?=E5=88=86=E5=89=B2=E7=BA=BF=20+=20=E7=BA=A2=E8=89=B2=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E5=AF=B9=E9=BD=90=E5=BE=AE=E4=BF=A1=EF=BC=9BMENU=5FKE?= =?UTF-8?q?YS=20=E6=8A=BD=20const=20=E9=98=B2=20typo=EF=BC=9B=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E5=9D=97=E4=BB=8E=E6=B0=94=E6=B3=A1=E4=B8=8A=E6=96=B9?= =?UTF-8?q?=E7=A7=BB=E5=88=B0=E4=B8=8B=E6=96=B9=EF=BC=8CselfSend=20?= =?UTF-8?q?=E6=97=B6=E7=AB=96=E7=BA=BF=E9=95=9C=E5=83=8F=E5=88=B0=E5=8F=B3?= =?UTF-8?q?=E4=BE=A7=20-=20MessagePanel=EF=BC=9AscrollToBottom=20=E6=94=B9?= =?UTF-8?q?=20async=20+=20waitMediaSettled=20=E7=AD=89=E5=9B=BE=E7=89=87?= =?UTF-8?q?=20/=20=E8=A7=86=E9=A2=91=E5=85=83=E6=95=B0=E6=8D=AE=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=EF=BC=9B=E7=94=A8=20expectedScrollTop=20drift=20?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20distanceFromBottom=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=8C=E5=9B=BE=E7=89=87=E5=8A=A0=E8=BD=BD=E5=AE=8C?= =?UTF-8?q?=E5=BA=95=E9=83=A8=E4=B8=8A=E7=A7=BB=E3=80=81=E8=AF=AF=E5=88=A4?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=B7=B2=E6=BB=9A=E8=B5=B0=E3=80=8D=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=88=B0=E4=B8=8D=E4=BA=86=E5=BA=95=20-=20ReplyPrevie?= =?UTF-8?q?w=EF=BC=9A=E5=88=A0=E7=AD=89=E4=BB=B7=E7=9A=84=20filePayload=20?= =?UTF-8?q?/=20voicePayload=20alias=EF=BC=8C=E7=9B=B4=E6=8E=A5=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=20parsedPayload=20-=20uiStore=EF=BC=9AContextMenuItem?= =?UTF-8?q?=20=E5=8A=A0=20icon=3F=20=E5=AD=97=E6=AE=B5=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=8F=9C=E5=8D=95=E9=A1=B9=E5=89=8D=E7=BD=AE=E5=9B=BE?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/message/MessageItem.vue | 72 ++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/views/im/home/pages/conversation/components/message/MessageItem.vue b/src/views/im/home/pages/conversation/components/message/MessageItem.vue index 8a7627144..e3fa84dd8 100644 --- a/src/views/im/home/pages/conversation/components/message/MessageItem.vue +++ b/src/views/im/home/pages/conversation/components/message/MessageItem.vue @@ -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 + /** * 右键菜单项: - * - 回复:仅已落库(id≠0)且未撤回的消息可引用,引用块写入 draftStore.reply - * - 删除:从本地消息列表移除(不动后端) - * - 撤回:仅自己发送、已送达(有 id)的消息 + * - 引用:已落库(id≠0)+ 未撤回的消息可引用,引用块写入 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 }> = [] - // "回复"必须满足 已落库(id≠0) + 未撤回;本地占位消息不允许引用,避免引用一条还没拿到 id 的消息 - // TODO @AI:应该是“引用”。你看看注释,中文,是不是都要调整下。 + const items: Array<{ + key: MenuKey + name: string + disabled?: boolean + divided?: boolean + danger?: boolean + icon?: string + }> = [] + // 「引用」:已落库(id≠0)+ 未撤回;本地占位消息(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: '撤回' }) + // 「撤回 / 删除」二选一: + // - 自己发送 + 已落库(id≠0)+ 未撤回 + 在撤回窗口内 → 撤回(推服务器把消息态置 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 + }) } - // "删除"对所有消息开放(纯本地清理,无后端影响);"撤回"必须满足 自己发 + 已落库(id≠0)+ 未撤回 - // TODO @AI:删除应该有个 --- 横线;然后是红色的,对齐微信; - items.push({ key: 'DELETE', name: '删除' }) + // 把菜单渲染交给全局 uiStore(单例,避免每条消息都挂一份菜单 DOM);callback 按 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() } })