diff --git a/resources/js/chat-room/right-panel.js b/resources/js/chat-room/right-panel.js index 3b903d7..6260a6e 100644 --- a/resources/js/chat-room/right-panel.js +++ b/resources/js/chat-room/right-panel.js @@ -1,9 +1,75 @@ // 聊天室右侧名单/房间面板事件绑定,逐步替代 Blade 内联事件。 let rightPanelEventsBound = false; +// 用户名单由 Blade 主脚本动态重建,点击/双击状态必须放在模块层避免 DOM 重建后残留闭包。 +const userListClickTimers = new WeakMap(); +const userListLastTapTimes = new WeakMap(); +const USER_LIST_CLICK_DELAY = 250; +const USER_LIST_DOUBLE_TAP_DELAY = 300; /** - * 绑定右侧在线面板的 tab、刷新、排序和搜索事件。 + * 选择聊天目标并聚焦输入框。 + * + * @param {string} username 目标用户名 + * @returns {void} + */ +function selectChatTarget(username) { + const toUserSelect = document.getElementById("to_user"); + if (toUserSelect instanceof HTMLSelectElement) { + toUserSelect.value = username; + } + + // 手机抽屉里的名单点击后需要收起抽屉,桌面端没有该函数时自然跳过。 + window.closeMobileDrawer?.(); + document.getElementById("content")?.focus(); +} + +/** + * 打开用户资料卡并取消待执行的单击选择。 + * + * @param {HTMLElement} item 用户列表项 + * @returns {void} + */ +function openUserCardFromList(item) { + const username = item.dataset.username || ""; + const clickTimer = userListClickTimers.get(item); + + if (!username) { + return; + } + + if (clickTimer) { + window.clearTimeout(clickTimer); + userListClickTimers.delete(item); + } + + window.closeMobileDrawer?.(); + window.openUserCard?.(username); +} + +/** + * 调度用户列表单击选择,延迟执行用于和双击打开名片互斥。 + * + * @param {HTMLElement} item 用户列表项 + * @returns {void} + */ +function scheduleUserListSelection(item) { + const username = item.dataset.username || ""; + + if (!username || userListClickTimers.has(item)) { + return; + } + + const clickTimer = window.setTimeout(() => { + userListClickTimers.delete(item); + selectChatTarget(username); + }, USER_LIST_CLICK_DELAY); + + userListClickTimers.set(item, clickTimer); +} + +/** + * 绑定右侧在线面板的 tab、刷新、排序、搜索与用户列表选择事件。 * * @returns {void} */ @@ -31,9 +97,58 @@ export function bindChatRightPanelControls() { if (refreshButton) { event.preventDefault(); window.renderUserList?.(); + return; + } + + const everyoneItem = event.target.closest("[data-user-list-everyone]"); + if (everyoneItem) { + event.preventDefault(); + selectChatTarget("大家"); + return; + } + + const userItem = event.target.closest("[data-user-list-entry]"); + if (userItem instanceof HTMLElement) { + event.preventDefault(); + scheduleUserListSelection(userItem); } }); + document.addEventListener("dblclick", (event) => { + if (!(event.target instanceof Element)) { + return; + } + + const userItem = event.target.closest("[data-user-list-entry]"); + if (userItem instanceof HTMLElement) { + event.preventDefault(); + openUserCardFromList(userItem); + } + }); + + document.addEventListener("touchend", (event) => { + if (!(event.target instanceof Element)) { + return; + } + + const userItem = event.target.closest("[data-user-list-entry]"); + if (!(userItem instanceof HTMLElement)) { + return; + } + + // 移动端双触发沿用旧的 300ms 口径,单触摸仍交给 click 事件选择聊天目标。 + const now = Date.now(); + const lastTapTime = userListLastTapTimes.get(userItem) || 0; + if (now - lastTapTime < USER_LIST_DOUBLE_TAP_DELAY) { + event.preventDefault(); + userListLastTapTimes.delete(userItem); + openUserCardFromList(userItem); + return; + } + + userListLastTapTimes.set(userItem, now); + }, { passive: false }); + document.addEventListener("change", (event) => { if (!(event.target instanceof HTMLSelectElement) || event.target.id !== "user-sort-select") { return; diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 4bb7494..86de7c9 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -1931,10 +1931,8 @@ // 在列表顶部添加"大家"条目(原版风格) let allDiv = document.createElement('div'); allDiv.className = 'user-item'; + allDiv.dataset.userListEveryone = '1'; allDiv.innerHTML = '大家'; - allDiv.onclick = () => { - toUserSelect.value = '大家'; - }; fragment.appendChild(allDiv); // ── AI 小助手(已在 onlineUsers 中动态维护,移除硬编码)── @@ -1963,6 +1961,7 @@ let item = document.createElement('div'); item.className = 'user-item'; item.dataset.username = username; + item.dataset.userListEntry = '1'; const headface = (user.headface || '1.gif'); const headImgSrc = headface.startsWith('storage/') ? '/' + headface : '/images/headface/' + @@ -1978,41 +1977,7 @@ ${badges} `; - // 单击/双击互斥:单击延迟 250ms 执行,双击取消单击定时器后直接执行双击逻辑 - let _clickTimer = null; - item.onclick = () => { - if (_clickTimer) { - return; - } - _clickTimer = setTimeout(() => { - _clickTimer = null; - toUserSelect.value = username; - // 手机端:点击名字时关闭名单抽屉 - if (typeof closeMobileDrawer === 'function') { - closeMobileDrawer(); - } - document.getElementById('content').focus(); - }, 250); - }; - // 双击 / 手机端双触发 打开用户名片弹窗 - const _openCardFromList = () => { - if (_clickTimer) { clearTimeout(_clickTimer); _clickTimer = null; } - if (typeof closeMobileDrawer === 'function') { closeMobileDrawer(); } - openUserCard(username); - }; - item.ondblclick = _openCardFromList; - // 手机端:检测 300ms 内两次 touchend 触发双击 - let _tapTime = 0; - item.addEventListener('touchend', (e) => { - const now = Date.now(); - if (now - _tapTime < 300) { - e.preventDefault(); - _openCardFromList(); - _tapTime = 0; - } else { - _tapTime = now; - } - }, { passive: false }); + // 具体点击、双击与手机双触发由 Vite 的 right-panel.js 统一事件委托处理。 fragment.appendChild(item); });