diff --git a/resources/js/chat-room/right-panel.js b/resources/js/chat-room/right-panel.js
index 6260a6e..b46cdf6 100644
--- a/resources/js/chat-room/right-panel.js
+++ b/resources/js/chat-room/right-panel.js
@@ -4,8 +4,12 @@ let rightPanelEventsBound = false;
// 用户名单由 Blade 主脚本动态重建,点击/双击状态必须放在模块层避免 DOM 重建后残留闭包。
const userListClickTimers = new WeakMap();
const userListLastTapTimes = new WeakMap();
+const messageUserClickTimers = new WeakMap();
+const messageUserLastTapTimes = new WeakMap();
const USER_LIST_CLICK_DELAY = 250;
const USER_LIST_DOUBLE_TAP_DELAY = 300;
+const MESSAGE_USER_CLICK_DELAY = 250;
+const MESSAGE_USER_DOUBLE_TAP_DELAY = 300;
/**
* 选择聊天目标并聚焦输入框。
@@ -47,6 +51,78 @@ function openUserCardFromList(item) {
window.openUserCard?.(username);
}
+/**
+ * 读取消息区用户名,过滤掉系统播报和游戏标签等不可交互内容。
+ *
+ * @param {HTMLElement} item 消息区用户名节点
+ * @returns {string}
+ */
+function getMessageUsername(item) {
+ return (item.dataset.u || "").trim();
+}
+
+/**
+ * 消息区用户名单击选择聊天目标,保留旧 switchTarget 的离线用户兜底逻辑。
+ *
+ * @param {string} username 目标用户名
+ * @returns {void}
+ */
+function selectMessageChatTarget(username) {
+ if (!username) {
+ return;
+ }
+
+ if (typeof window.switchTarget === "function") {
+ window.switchTarget(username);
+ return;
+ }
+
+ selectChatTarget(username);
+}
+
+/**
+ * 消息区用户名双击打开名片,并取消待执行的单击选择。
+ *
+ * @param {HTMLElement} item 消息区用户名节点
+ * @returns {void}
+ */
+function openUserCardFromMessage(item) {
+ const username = getMessageUsername(item);
+ const clickTimer = messageUserClickTimers.get(item);
+
+ if (!username) {
+ return;
+ }
+
+ if (clickTimer) {
+ window.clearTimeout(clickTimer);
+ messageUserClickTimers.delete(item);
+ }
+
+ window.openUserCard?.(username);
+}
+
+/**
+ * 调度消息区用户名单击选择,延迟执行用于和双击打开名片互斥。
+ *
+ * @param {HTMLElement} item 消息区用户名节点
+ * @returns {void}
+ */
+function scheduleMessageUserSelection(item) {
+ const username = getMessageUsername(item);
+
+ if (!username || messageUserClickTimers.has(item)) {
+ return;
+ }
+
+ const clickTimer = window.setTimeout(() => {
+ messageUserClickTimers.delete(item);
+ selectMessageChatTarget(username);
+ }, MESSAGE_USER_CLICK_DELAY);
+
+ messageUserClickTimers.set(item, clickTimer);
+}
+
/**
* 调度用户列表单击选择,延迟执行用于和双击打开名片互斥。
*
@@ -111,6 +187,13 @@ export function bindChatRightPanelControls() {
if (userItem instanceof HTMLElement) {
event.preventDefault();
scheduleUserListSelection(userItem);
+ return;
+ }
+
+ const messageUser = event.target.closest("[data-chat-message-user]");
+ if (messageUser instanceof HTMLElement) {
+ event.preventDefault();
+ scheduleMessageUserSelection(messageUser);
}
});
@@ -123,6 +206,13 @@ export function bindChatRightPanelControls() {
if (userItem instanceof HTMLElement) {
event.preventDefault();
openUserCardFromList(userItem);
+ return;
+ }
+
+ const messageUser = event.target.closest("[data-chat-message-user]");
+ if (messageUser instanceof HTMLElement) {
+ event.preventDefault();
+ openUserCardFromMessage(messageUser);
}
});
@@ -133,6 +223,22 @@ export function bindChatRightPanelControls() {
const userItem = event.target.closest("[data-user-list-entry]");
if (!(userItem instanceof HTMLElement)) {
+ const messageUser = event.target.closest("[data-chat-message-user]");
+ if (!(messageUser instanceof HTMLElement)) {
+ return;
+ }
+
+ // 消息区用户名由 Blade 渲染,移动端双触发统一在 Vite 模块内处理。
+ const now = Date.now();
+ const lastTapTime = messageUserLastTapTimes.get(messageUser) || 0;
+ if (now - lastTapTime < MESSAGE_USER_DOUBLE_TAP_DELAY) {
+ event.preventDefault();
+ messageUserLastTapTimes.delete(messageUser);
+ openUserCardFromMessage(messageUser);
+ return;
+ }
+
+ messageUserLastTapTimes.set(messageUser, now);
return;
}
diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php
index 0caeee0..edeb493 100644
--- a/resources/views/chat/partials/scripts.blade.php
+++ b/resources/views/chat/partials/scripts.blade.php
@@ -43,33 +43,6 @@
const hoverTooltip = document.getElementById('chat-hover-tooltip');
let activeTooltipTrigger = null;
- // ── 消息区:手机端双触发打开用户名片(PC 端靠 ondblclick 内联属性)──
- // span[data-u] 由 clickableUser() 生成,touchend 委托至容器避免每条消息单独绑定
- (function _bindMsgDoubleTap() {
- let _lastTapTarget = null;
- let _lastTapTime = 0;
-
- /** touchend 委托处理函数 */
- function _onMsgTouch(e) {
- const span = e.target.closest('[data-u]');
- if (!span) return;
- const now = Date.now();
- if (span === _lastTapTarget && now - _lastTapTime < 300) {
- e.preventDefault();
- openUserCard(span.dataset.u);
- _lastTapTarget = null;
- _lastTapTime = 0;
- } else {
- _lastTapTarget = span;
- _lastTapTime = now;
- }
- }
-
- // 两个聊天容器(公屏 + 包厢)都绑定
- [container, container2].forEach(c => {
- if (c) { c.addEventListener('touchend', _onMsgTouch, { passive: false }); }
- });
- })();
let onlineUsers = {};
// 暂时暴露给已迁移的手机抽屉 Vite 模块读取,后续在线名单整体迁移后可移除。
window.onlineUsers = onlineUsers;
@@ -2357,15 +2330,16 @@
return false;
};
- // 用户名(单击切换发言对象,双击查看资料;系统用户或游戏标签仅显示文本)
+ // 用户名(单击切换发言对象,双击查看资料;事件委托已迁至 Vite right-panel.js)
const clickableUser = (uName, color) => {
+ const safeName = escapeHtml(uName);
if (uName === 'AI小班长') {
- return `${uName}`;
+ return `${safeName}`;
}
if (systemUsers.includes(uName) || isGameLabel(uName)) {
- return `${uName}`;
+ return `${safeName}`;
}
- return `${uName}`;
+ return `${safeName}`;
};
// 普通用户(包括 AI小班长)用数据库头像,播报类用特殊喇叭图标
@@ -2702,29 +2676,7 @@
window.initChat(window.chatContext.roomId);
}
- // 手机端:在公屏(say1)和包厢(say2)容器上注册 touchend 委托
- // 检测 300ms 内两次触摸同一 [data-u] 用户名 span,触发 openUserCard
- let _msgTapTarget = null;
- let _msgTapTime = 0;
- ['say1', 'say2'].forEach(id => {
- const el = document.getElementById(id);
- if (!el) { return; }
- el.addEventListener('touchend', (e) => {
- const span = e.target.closest('[data-u]');
- if (!span) { return; }
- const uName = span.dataset.u;
- const now = Date.now();
- if (uName === _msgTapTarget && now - _msgTapTime < 300) {
- e.preventDefault();
- openUserCard(uName);
- _msgTapTarget = null;
- _msgTapTime = 0;
- } else {
- _msgTapTarget = uName;
- _msgTapTime = now;
- }
- }, { passive: false });
- });
+ // 消息区用户名点击/双击/移动端双触发已迁至 Vite 的 right-panel.js 统一委托。
});
// ── WebSocket 事件监听 ────────────────────────────