From 7f1fda40dcea22f265b194e80be7d461a3c46679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Thu, 18 Dec 2025 15:09:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BAGroupContextMenu=E5=92=8CVirt?= =?UTF-8?q?ualContactList=E7=BB=84=E4=BB=B6=EF=BC=9A=E5=9C=A8GroupContextM?= =?UTF-8?q?enu=E4=B8=AD=E6=B7=BB=E5=8A=A0=E9=81=AE=E7=BD=A9=E5=B1=82?= =?UTF-8?q?=E5=8F=B3=E9=94=AE=E4=BA=8B=E4=BB=B6=E5=9B=9E=E8=B0=83=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=8F=9C=E5=8D=95=E5=9C=A8=E6=96=B0=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E6=89=93=E5=BC=80=EF=BC=8C=E4=BC=98=E5=8C=96SidebarMe?= =?UTF-8?q?nu=E4=B8=AD=E7=9A=84=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=BB=A5=E9=81=BF=E5=85=8D=E9=97=AA=E7=83=81?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/GroupContextMenu/index.tsx | 20 ++- .../components/VirtualContactList/index.tsx | 3 + .../SidebarMenu/WechatFriends/index.tsx | 122 ++++++++++++++++-- 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/Touchkebao/src/components/GroupContextMenu/index.tsx b/Touchkebao/src/components/GroupContextMenu/index.tsx index 0136d866..d1e62a40 100644 --- a/Touchkebao/src/components/GroupContextMenu/index.tsx +++ b/Touchkebao/src/components/GroupContextMenu/index.tsx @@ -32,6 +32,8 @@ export interface GroupContextMenuProps { onEditClick?: (group: ContactGroup) => void; /** 删除分组点击(由上层打开删除确认弹窗) */ onDeleteClick?: (group: ContactGroup) => void; + /** 遮罩层右键事件回调(用于在新位置打开菜单) */ + onOverlayContextMenu?: (e: React.MouseEvent) => void; } /** @@ -48,6 +50,7 @@ export const GroupContextMenu: React.FC = ({ onAddClick, onEditClick, onDeleteClick, + onOverlayContextMenu, }) => { // 默认群分组 / 未分组:id 为 0,且 groupType 为 1 或 2 const isDefaultOrUngrouped = @@ -106,12 +109,27 @@ export const GroupContextMenu: React.FC = ({ if (!visible) return null; + // 处理遮罩层右键事件:关闭菜单并通知父组件处理新位置的右键事件 + const handleOverlayContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + // 关闭当前菜单 + onClose(); + // 通知父组件处理新位置的右键事件 + if (onOverlayContextMenu) { + // 使用 setTimeout 确保菜单先关闭,然后再处理新事件 + setTimeout(() => { + onOverlayContextMenu(e); + }, 0); + } + }; + return ( <>
e.preventDefault()} + onContextMenu={handleOverlayContextMenu} /> = ({
onGroupToggle?.(group.id, group.groupType)} onContextMenu={e => onGroupContextMenu?.(e, group)} > diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx index 34d1946a..1c2e1330 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx @@ -162,15 +162,120 @@ const ContactListSimple: React.FC = ({ (e: React.MouseEvent, group: ContactGroup) => { e.preventDefault(); e.stopPropagation(); - setGroupContextMenu({ - visible: true, - x: e.clientX, - y: e.clientY, - group, - groupType: group.groupType, - }); + + // 如果已经有菜单打开,先关闭它,然后在下一个渲染周期打开新菜单 + if (groupContextMenu.visible) { + setGroupContextMenu({ + visible: false, + x: 0, + y: 0, + }); + // 使用 requestAnimationFrame 确保关闭操作先执行,然后再打开新菜单 + // 这样可以避免菜单闪烁,提供更流畅的体验 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setGroupContextMenu({ + visible: true, + x: e.clientX, + y: e.clientY, + group, + groupType: group.groupType, + }); + }); + }); + } else { + // 如果没有菜单打开,直接打开新菜单 + setGroupContextMenu({ + visible: true, + x: e.clientX, + y: e.clientY, + group, + groupType: group.groupType, + }); + } }, - [], + [groupContextMenu.visible], + ); + + // 处理遮罩层右键事件:根据鼠标位置找到对应的群组并打开菜单 + const handleOverlayContextMenu = useCallback( + (e: React.MouseEvent) => { + // 菜单已经关闭,现在需要找到鼠标位置下的群组元素 + // 使用 setTimeout 确保遮罩层已经移除,DOM 更新完成 + setTimeout(() => { + // 获取当前要显示的群组列表(优先使用新架构的groups) + const currentDisplayGroups = + newGroups.length > 0 + ? newGroups + : contactGroups.map((g: ContactGroupByLabel) => ({ + id: g.id, + groupName: g.groupName, + groupType: g.groupType as 1 | 2, + count: g.count, + sort: g.sort, + groupMemo: g.groupMemo, + })); + + // 方法1:尝试通过 data-group-key 属性直接找到群组元素 + const element = document.elementFromPoint(e.clientX, e.clientY); + if (!element) return; + + // 向上查找群组头部元素 + let groupElement: HTMLElement | null = element as HTMLElement; + while (groupElement) { + // 检查是否有 data-group-key 属性 + const groupKey = groupElement.getAttribute('data-group-key'); + if (groupKey) { + // 解析 groupKey 获取群组信息 + const [groupId, groupType] = groupKey.split('_').map(Number); + const group = currentDisplayGroups.find( + g => g.id === groupId && g.groupType === groupType + ); + if (group) { + // 创建合成事件并触发群组右键菜单 + const syntheticEvent = { + ...e, + preventDefault: () => {}, + stopPropagation: () => {}, + clientX: e.clientX, + clientY: e.clientY, + } as React.MouseEvent; + handleGroupContextMenu(syntheticEvent, group); + return; + } + } + groupElement = groupElement.parentElement; + } + + // 方法2:如果方法1失败,遍历所有群组,检查鼠标位置是否在群组头部范围内 + for (const group of currentDisplayGroups) { + const groupKey = getGroupKey(group.id, group.groupType, selectedAccountId); + const groupHeaderElement = document.querySelector( + `[data-group-key="${groupKey}"]` + ) as HTMLElement; + if (groupHeaderElement) { + const rect = groupHeaderElement.getBoundingClientRect(); + if ( + e.clientX >= rect.left && + e.clientX <= rect.right && + e.clientY >= rect.top && + e.clientY <= rect.bottom + ) { + const syntheticEvent = { + ...e, + preventDefault: () => {}, + stopPropagation: () => {}, + clientX: e.clientX, + clientY: e.clientY, + } as React.MouseEvent; + handleGroupContextMenu(syntheticEvent, group); + return; + } + } + } + }, 50); // 给足够的时间让遮罩层移除 + }, + [newGroups, contactGroups, selectedAccountId, getGroupKey, handleGroupContextMenu], ); // 打开新增分组弹窗 @@ -666,6 +771,7 @@ const ContactListSimple: React.FC = ({ onAddClick={handleOpenAddGroupModal} onEditClick={handleOpenEditGroupModal} onDeleteClick={handleOpenDeleteGroupModal} + onOverlayContextMenu={handleOverlayContextMenu} /> {/* 联系人右键菜单 */}