From 17d1885efcb6938a320d97e7c124980fcc094762 Mon Sep 17 00:00:00 2001 From: lkddi Date: Sat, 25 Apr 2026 19:50:28 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=9C=A8=E7=BA=BF=E5=90=8D?= =?UTF-8?q?=E5=8D=95=E6=8F=90=E7=A4=BA=E6=B0=94=E6=B3=A1=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/chat-room.js | 9 ++ resources/js/chat-room/hover-tooltip.js | 141 ++++++++++++++++++ .../views/chat/partials/scripts.blade.php | 101 ------------- 3 files changed, 150 insertions(+), 101 deletions(-) create mode 100644 resources/js/chat-room/hover-tooltip.js diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 2b7920e..4a4e713 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -249,6 +249,9 @@ export { updateRedPacketClaimsUI, } from "./chat-room/red-packet-panel.js"; export { createMessageQueue } from "./chat-room/message-queue.js"; +export { + bindInstantHoverTooltip, +} from "./chat-room/hover-tooltip.js"; export { isExpiredChatImageMessage, localClearScreen, @@ -448,6 +451,9 @@ import { updateRedPacketClaimsUI, } from "./chat-room/red-packet-panel.js"; import { createMessageQueue } from "./chat-room/message-queue.js"; +import { + bindInstantHoverTooltip, +} from "./chat-room/hover-tooltip.js"; import { isExpiredChatImageMessage, localClearScreen, @@ -457,6 +463,8 @@ import { } from "./chat-room/message-utils.js"; if (typeof window !== "undefined") { + bindInstantHoverTooltip(); + // 保留聚合入口,给新迁移模块、测试和仍在 Blade 内的存量脚本统一读取工具。 window.ChatRoomTools = { escapeHtml, @@ -680,6 +688,7 @@ if (typeof window !== "undefined") { showRedPacketModal, updateRedPacketClaimsUI, createMessageQueue, + bindInstantHoverTooltip, isExpiredChatImageMessage, localClearScreen, scrollChatToBottom, diff --git a/resources/js/chat-room/hover-tooltip.js b/resources/js/chat-room/hover-tooltip.js new file mode 100644 index 0000000..7d5fc61 --- /dev/null +++ b/resources/js/chat-room/hover-tooltip.js @@ -0,0 +1,141 @@ +// 在线名单徽章即时提示工具,负责动态徽章的悬浮气泡定位和显示。 + +const TOOLTIP_SELECTOR = ".user-badge-icon[data-instant-tooltip]"; + +/** + * 绑定在线名单徽章的即时悬浮提示。 + * Vite 模块脚本通常晚于 Blade 经典脚本执行,这里由模块加载后主动绑定,避免依赖 Blade 调用时序。 + * + * @param {HTMLElement|null} tooltip 提示气泡容器 + * @param {Document|HTMLElement} root 事件委托根节点 + * @returns {{hide: () => void, destroy: () => void}|null} + */ +export function bindInstantHoverTooltip( + tooltip = document.getElementById("chat-hover-tooltip"), + root = document, +) { + if (!tooltip || tooltip.dataset.instantTooltipBound === "1") { + return null; + } + + tooltip.dataset.instantTooltipBound = "1"; + let activeTrigger = null; + + /** + * 从事件目标解析可显示提示的徽章节点。 + * + * @param {EventTarget|null} target 事件目标 + * @returns {HTMLElement|null} + */ + const resolveTrigger = (target) => { + if (!(target instanceof Element)) { + return null; + } + + return target.closest(TOOLTIP_SELECTOR); + }; + + /** + * 把提示气泡定位到徽章旁边,同时避免超出视口边缘。 + * + * @param {HTMLElement|null} trigger 当前悬浮徽章 + * @returns {void} + */ + const position = (trigger) => { + if (!trigger) { + return; + } + + const offset = 10; + const rect = trigger.getBoundingClientRect(); + const tooltipWidth = tooltip.offsetWidth; + const tooltipHeight = tooltip.offsetHeight; + const fitsRight = rect.right + offset + tooltipWidth <= window.innerWidth - 8; + const side = fitsRight ? "right" : "left"; + const nextLeft = fitsRight ? rect.right + offset : Math.max(8, rect.left - tooltipWidth - offset); + const nextTop = Math.min( + Math.max(8, rect.top + (rect.height - tooltipHeight) / 2), + window.innerHeight - tooltipHeight - 8, + ); + + tooltip.dataset.side = side; + tooltip.style.left = `${nextLeft}px`; + tooltip.style.top = `${nextTop}px`; + }; + + /** + * 显示当前徽章对应的提示文字。 + * + * @param {HTMLElement|null} trigger 当前悬浮徽章 + * @returns {void} + */ + const show = (trigger) => { + const tooltipText = trigger?.dataset.instantTooltip || ""; + + if (!trigger || !tooltipText) { + return; + } + + activeTrigger = trigger; + tooltip.textContent = tooltipText; + tooltip.style.display = "block"; + position(trigger); + }; + + /** + * 隐藏提示气泡并清理方向状态。 + * + * @returns {void} + */ + const hide = () => { + activeTrigger = null; + tooltip.style.display = "none"; + tooltip.textContent = ""; + delete tooltip.dataset.side; + }; + + const handleMouseOver = (event) => { + const trigger = resolveTrigger(event.target); + + if (trigger) { + show(trigger); + } + }; + + const handleMouseOut = (event) => { + const trigger = resolveTrigger(event.target); + + if (!trigger || trigger !== activeTrigger) { + return; + } + + if (event.relatedTarget && trigger.contains(event.relatedTarget)) { + return; + } + + hide(); + }; + + const reposition = () => { + if (activeTrigger) { + position(activeTrigger); + } + }; + + root.addEventListener("mouseover", handleMouseOver); + root.addEventListener("mouseout", handleMouseOut); + window.addEventListener("scroll", reposition, true); + window.addEventListener("resize", reposition); + + return { + hide, + destroy() { + root.removeEventListener("mouseover", handleMouseOver); + root.removeEventListener("mouseout", handleMouseOut); + window.removeEventListener("scroll", reposition, true); + window.removeEventListener("resize", reposition); + delete tooltip.dataset.instantTooltipBound; + hide(); + }, + }; +} diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index cb26c00..a3221fc 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -40,8 +40,6 @@ const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = window.ChatRoomTools?.BLOCKED_SYSTEM_SENDERS_STORAGE_KEY || 'chat_blocked_system_senders'; const CHAT_SOUND_MUTED_STORAGE_KEY = window.ChatRoomTools?.CHAT_SOUND_MUTED_STORAGE_KEY || 'chat_sound_muted'; const BLOCKABLE_SYSTEM_SENDERS = window.ChatRoomTools?.BLOCKABLE_SYSTEM_SENDERS || ['钓鱼播报', '星海小博士', '百家乐', '跑马', '神秘箱子']; - const hoverTooltip = document.getElementById('chat-hover-tooltip'); - let activeTooltipTrigger = null; let onlineUsers = {}; // 暂时暴露给已迁移的手机抽屉 Vite 模块读取,后续在线名单整体迁移后可移除。 @@ -1780,105 +1778,6 @@ } } - /** - * 把小气泡提示定位到图标旁边,不侵入右侧名单结构。 - * - * @param {HTMLElement} trigger 当前悬浮的图标元素 - */ - function positionHoverTooltip(trigger) { - if (!hoverTooltip || !trigger) { - return; - } - - const offset = 10; - const rect = trigger.getBoundingClientRect(); - const tooltipWidth = hoverTooltip.offsetWidth; - const tooltipHeight = hoverTooltip.offsetHeight; - const fitsRight = rect.right + offset + tooltipWidth <= window.innerWidth - 8; - const side = fitsRight ? 'right' : 'left'; - const nextLeft = fitsRight - ? rect.right + offset - : Math.max(8, rect.left - tooltipWidth - offset); - const nextTop = Math.min( - Math.max(8, rect.top + (rect.height - tooltipHeight) / 2), - window.innerHeight - tooltipHeight - 8 - ); - - hoverTooltip.dataset.side = side; - hoverTooltip.style.left = `${nextLeft}px`; - hoverTooltip.style.top = `${nextTop}px`; - } - - /** - * 显示图标旁边的小气泡文字提示。 - * - * @param {HTMLElement|null} trigger 当前悬浮的图标元素 - */ - function showHoverTooltip(trigger) { - if (!hoverTooltip || !trigger) { - return; - } - - const tooltipText = trigger.dataset.instantTooltip || ''; - if (!tooltipText) { - return; - } - - activeTooltipTrigger = trigger; - hoverTooltip.textContent = tooltipText; - hoverTooltip.style.display = 'block'; - positionHoverTooltip(trigger); - } - - /** - * 隐藏图标提示气泡。 - */ - function hideHoverTooltip() { - if (!hoverTooltip) { - return; - } - - activeTooltipTrigger = null; - hoverTooltip.style.display = 'none'; - hoverTooltip.textContent = ''; - delete hoverTooltip.dataset.side; - } - - // 通过事件委托处理动态生成的徽章提示,确保 hover 后立刻显示。 - document.addEventListener('mouseover', (event) => { - const trigger = event.target.closest('.user-badge-icon[data-instant-tooltip]'); - if (!trigger) { - return; - } - - showHoverTooltip(trigger); - }); - - document.addEventListener('mouseout', (event) => { - const trigger = event.target.closest('.user-badge-icon[data-instant-tooltip]'); - if (!trigger || trigger !== activeTooltipTrigger) { - return; - } - - if (event.relatedTarget && trigger.contains(event.relatedTarget)) { - return; - } - - hideHoverTooltip(); - }); - - window.addEventListener('scroll', () => { - if (activeTooltipTrigger) { - positionHoverTooltip(activeTooltipTrigger); - } - }, true); - - window.addEventListener('resize', () => { - if (activeTooltipTrigger) { - positionHoverTooltip(activeTooltipTrigger); - } - }); - // ── 渲染在线人员列表(支持排序) ────────────────── /** * 核心渲染函数:将在线用户渲染到指定容器(桌面端名单区和手机端抽屉共用)