302 lines
15 KiB
PHP
302 lines
15 KiB
PHP
{{--
|
||
文件功能:聊天室核心前端交互脚本(Blade 模板形式)
|
||
|
||
已迁移至 Vite 模块(resources/js/chat-room/)的内容:
|
||
1. 消息渲染引擎 → message-renderer.js(appendMessage / buildChatMessageContent / 批量渲染 / 裁剪)
|
||
2. 在线用户列表 → user-list.js(renderUserList / filterUserList / 徽标轮换)
|
||
3. WebSocket 事件监听 → chat-events.js(chat:here / chat:message / chat:muted / Echo 级监听等)
|
||
4. 管理操作 → admin-commands.js + admin-menu.js
|
||
5. 存点心跳 → heartbeat.js(saveExp / leaveRoom / notifyExpiredLeave)
|
||
6. 发送消息 → composer.js(sendMessage / 草稿 / IME 防重 / 神秘箱子暗号)
|
||
7. 特效控制 → admin-commands.js + message-utils.js
|
||
8. 系统播报屏蔽 → preferences-status.js(屏蔽 / 禁音 / 每日状态 / 偏好)
|
||
9. 共享状态 → chat-state.js(DOM 引用 / 在线用户 / 消息队列 / 所有可变状态)
|
||
10. 欢迎语菜单 → welcome-menu.js
|
||
11. 每日签到 → daily-sign-in.js
|
||
12. VIP 进退场 → vip-presence.js
|
||
|
||
保留在 Blade 内的内容(依赖 Blade 模板语法 或 作为薄兼容桥):
|
||
1. switchTab / loadRoomsOnlineStatus(依赖 {{ route('chat.rooms-online-status') }})
|
||
2. sendWelcomeTpl(依赖 sendMessage 已迁至 Vite)
|
||
3. 点击空白关闭浮层(简单 DOM 事件)
|
||
4. 自动滚屏复选框绑定
|
||
5. DOMContentLoaded 偏好恢复(调用 Vite 模块函数)
|
||
6. 各类 window.* 兼容桥声明
|
||
|
||
通过 @include('chat.partials.scripts') 引入到 frame.blade.php
|
||
|
||
@author ChatRoom Laravel
|
||
@version 3.0.0 — 大量逻辑已迁至 Vite 模块,此文件仅保留 Blade 依赖的薄包装
|
||
--}}
|
||
|
||
<script>
|
||
/**
|
||
* 聊天室前端交互逻辑(薄包装版本)
|
||
* 核心引擎已迁至 Vite 模块 resources/js/chat-room/,此脚本仅保留 Blade 模板依赖的桥接代码。
|
||
*/
|
||
|
||
// ── Blade 依赖变量(需要模板语法生成)──
|
||
const _currentRoomId = {{ $room->id }};
|
||
|
||
// ── Tab 切换 ──────────────────────────────────────
|
||
let _roomsRefreshTimer = null;
|
||
let _roomsOnlineStatusCache = null;
|
||
let _roomsOnlineStatusCacheAt = 0;
|
||
const ROOMS_ONLINE_STATUS_CACHE_TTL = 10000;
|
||
|
||
/**
|
||
* 切换右侧面板 Tab(用户列表 / 房间列表)。
|
||
* @param {string} tab 'users' | 'rooms'
|
||
*/
|
||
function switchTab(tab) {
|
||
['users', 'rooms'].forEach(t => {
|
||
document.getElementById('panel-' + t).style.display = t === tab ? 'block' : 'none';
|
||
document.getElementById('tab-' + t)?.classList.toggle('active', t === tab);
|
||
});
|
||
if (tab === 'rooms') {
|
||
loadRoomsOnlineStatus();
|
||
clearInterval(_roomsRefreshTimer);
|
||
_roomsRefreshTimer = setInterval(() => loadRoomsOnlineStatus(true), 30000);
|
||
} else {
|
||
clearInterval(_roomsRefreshTimer);
|
||
_roomsRefreshTimer = null;
|
||
}
|
||
}
|
||
window.switchTab = switchTab;
|
||
|
||
/**
|
||
* 拉取所有房间在线人数并渲染到右侧面板。
|
||
* @param {boolean} forceRefresh
|
||
*/
|
||
function loadRoomsOnlineStatus(forceRefresh = false) {
|
||
const container = document.getElementById('rooms-online-list');
|
||
if (!container) return;
|
||
|
||
if (!forceRefresh && _roomsOnlineStatusCache && Date.now() - _roomsOnlineStatusCacheAt < ROOMS_ONLINE_STATUS_CACHE_TTL) {
|
||
renderRoomsOnlineStatus(_roomsOnlineStatusCache, container);
|
||
return;
|
||
}
|
||
|
||
fetch('{{ route('chat.rooms-online-status') }}')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
_roomsOnlineStatusCache = data;
|
||
_roomsOnlineStatusCacheAt = Date.now();
|
||
renderRoomsOnlineStatus(data, container);
|
||
})
|
||
.catch(() => {
|
||
container.innerHTML = '<div style="text-align:center;color:#f00;padding:10px;font-size:11px;">加载失败</div>';
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 渲染房间在线状态列表(优先使用 Vite 模块)。
|
||
*/
|
||
function renderRoomsOnlineStatus(data, container) {
|
||
if (window.ChatRoomTools?.renderRoomsOnlineStatusToContainer) {
|
||
window.ChatRoomTools.renderRoomsOnlineStatusToContainer(data, container, {
|
||
currentRoomId: _currentRoomId,
|
||
variant: 'desktop',
|
||
});
|
||
return;
|
||
}
|
||
container.innerHTML = '<div style="text-align:center;color:#bbb;padding:16px 0;font-size:11px;">暂无房间</div>';
|
||
}
|
||
|
||
// ── 发送欢迎语模板 ──────────────────────────────────
|
||
|
||
/**
|
||
* 将选中的欢迎语模板填入输入框并自动发送。
|
||
* @param {string} tpl 欢迎语模板,含 {name} 占位符
|
||
*/
|
||
function sendWelcomeTpl(tpl) {
|
||
const toUser = document.getElementById('to_user')?.value || '大家';
|
||
if (toUser === '大家') {
|
||
window.chatDialog?.alert('欢迎语不能对「大家」发送,请先选择具体用户。', '🚫 提示', '#cc4444');
|
||
return;
|
||
}
|
||
const name = toUser;
|
||
const prefix = window.chatContext?.welcomePrefix || window.chatContext?.username || '';
|
||
const body = tpl.replace(/\{name\}/g, name);
|
||
const msg = `${prefix}:${body}`;
|
||
|
||
const input = document.getElementById('content');
|
||
if (input) input.value = msg;
|
||
|
||
const menu = document.getElementById('welcome-menu');
|
||
if (menu) menu.style.display = 'none';
|
||
|
||
const actionSel = document.getElementById('action');
|
||
const prevAction = actionSel?.value || '';
|
||
if (actionSel) actionSel.value = '欢迎';
|
||
|
||
// sendMessage 已由 composer.js(Vite)挂载到 window
|
||
window.sendMessage?.(null)?.finally(() => {
|
||
if (actionSel) actionSel.value = prevAction;
|
||
});
|
||
}
|
||
window.sendWelcomeTpl = sendWelcomeTpl;
|
||
|
||
// ── 点击空白关闭浮层 ──────────────────────────────
|
||
document.addEventListener('click', function(event) {
|
||
const clickedInsideFloatingMenu = event.target instanceof Element
|
||
&& event.target.closest('[data-chat-welcome-menu], [data-chat-admin-menu], [data-chat-block-menu], [data-chat-feature-menu], [data-chat-feature-menu-toggle], [data-chat-block-menu-toggle], [data-chat-welcome-menu-toggle], [data-chat-admin-menu-toggle]');
|
||
|
||
if (clickedInsideFloatingMenu) return;
|
||
|
||
['welcome-menu', 'admin-menu', 'block-menu', 'feature-menu', 'daily-status-editor-overlay'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.style.display = 'none';
|
||
});
|
||
});
|
||
|
||
// ── 自动滚屏复选框绑定 ──────────────────────────────
|
||
const autoScrollEl = document.getElementById('auto_scroll');
|
||
if (autoScrollEl) {
|
||
autoScrollEl.addEventListener('change', function() {
|
||
window.setChatAutoScrollEnabled?.(this.checked);
|
||
});
|
||
}
|
||
|
||
// ── 页面加载后从 localStorage 恢复偏好 ──────────────────
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
window.ChatRoomTools?.restoreChatFontSize?.();
|
||
|
||
const CHAT_SOUND_MUTED_KEY = window.ChatRoomTools?.CHAT_SOUND_MUTED_STORAGE_KEY || 'chat_sound_muted';
|
||
const BLOCKED_SENDERS_KEY = window.ChatRoomTools?.BLOCKED_SYSTEM_SENDERS_STORAGE_KEY || 'chat_blocked_system_senders';
|
||
const BLOCKABLE_SENDERS = window.ChatRoomTools?.BLOCKABLE_SYSTEM_SENDERS || ['钓鱼播报', '星海小博士', '百家乐', '跑马', '神秘箱子'];
|
||
|
||
const initialChatPreferences = window.ChatRoomTools?.normalizeChatPreferences
|
||
? window.ChatRoomTools.normalizeChatPreferences(window.chatContext?.chatPreferences || {}, BLOCKABLE_SENDERS)
|
||
: { blocked_system_senders: [], sound_muted: false };
|
||
|
||
let storedBlockedSenders = [];
|
||
try {
|
||
const saved = JSON.parse(localStorage.getItem(BLOCKED_SENDERS_KEY) || '[]');
|
||
storedBlockedSenders = Array.isArray(saved)
|
||
? saved.filter(s => BLOCKABLE_SENDERS.includes(s))
|
||
: [];
|
||
} catch (_) { /* ignore */ }
|
||
|
||
const mutedFromLocal = localStorage.getItem(CHAT_SOUND_MUTED_KEY) === '1';
|
||
|
||
const shouldMigrate = window.ChatRoomTools?.shouldMigrateLocalChatPreferences
|
||
? window.ChatRoomTools.shouldMigrateLocalChatPreferences(initialChatPreferences, storedBlockedSenders, mutedFromLocal)
|
||
: !(initialChatPreferences.blocked_system_senders.length > 0 || initialChatPreferences.sound_muted)
|
||
&& (storedBlockedSenders.length > 0 || mutedFromLocal);
|
||
|
||
// 恢复屏蔽发送者
|
||
if (shouldMigrate && storedBlockedSenders.length > 0) {
|
||
window.chatState.blockedSystemSenders = new Set(storedBlockedSenders);
|
||
} else if (initialChatPreferences.blocked_system_senders.length > 0) {
|
||
window.chatState.blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
|
||
}
|
||
|
||
// 恢复后立即隐藏已有消息
|
||
if (window.chatState.blockedSystemSenders.size > 0) {
|
||
const setRendered = window.setRenderedMessagesVisibilityBySender;
|
||
if (typeof setRendered === 'function') {
|
||
window.chatState.blockedSystemSenders.forEach(function (sender) {
|
||
setRendered(sender, true);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 恢复禁音状态
|
||
const muted = shouldMigrate ? mutedFromLocal : initialChatPreferences.sound_muted;
|
||
if (window.ChatRoomTools?.setSoundMuted) {
|
||
window.ChatRoomTools.setSoundMuted(muted);
|
||
} else {
|
||
const muteChk = document.getElementById('sound_muted');
|
||
if (muteChk) muteChk.checked = muted;
|
||
}
|
||
|
||
// 同步屏蔽复选框
|
||
if (typeof window.syncBlockedSystemSenderCheckboxes === 'function') {
|
||
window.syncBlockedSystemSenderCheckboxes();
|
||
}
|
||
|
||
if (shouldMigrate && typeof window.saveChatPreferences === 'function') {
|
||
void window.saveChatPreferences();
|
||
} else if (typeof window.persistChatPreferencesToLocal === 'function') {
|
||
window.persistChatPreferencesToLocal();
|
||
}
|
||
});
|
||
|
||
// ── window.* 兼容桥声明 ──────────────────────────────
|
||
// 以下函数已全部迁至 Vite 模块,这里仅做 window 引用声明,
|
||
// 确保 Blade 内仍可直接调用这些函数名(无需 window. 前缀)。
|
||
|
||
// 共享状态桥接:将 Vite 的 chatState 暴露为 Blade 兼容的本地引用
|
||
const chatState = window.chatState || {};
|
||
|
||
// 这些函数由 message-renderer.js 挂载到 window
|
||
const appendMessage = (...args) => window.appendMessage(...args);
|
||
const buildChatMessageContent = (...args) => window.buildChatMessageContent(...args);
|
||
const pruneMessageContainer = (...args) => window.pruneMessageContainer(...args);
|
||
const enqueueChatMessage = (...args) => window.enqueueChatMessage(...args);
|
||
const flushQueuedChatMessages = (...args) => window.flushQueuedChatMessages(...args);
|
||
const createChatMessageRenderBatch = (...args) => window.createChatMessageRenderBatch(...args);
|
||
const commitChatMessageRenderBatch = (...args) => window.commitChatMessageRenderBatch(...args);
|
||
|
||
// 这些函数由 user-list.js 挂载到 window
|
||
const renderUserList = (...args) => window.renderUserList(...args);
|
||
const scheduleRenderUserList = (...args) => window.scheduleRenderUserList(...args);
|
||
const filterUserList = (...args) => window.filterUserList(...args);
|
||
const scheduleFilterUserList = (...args) => window.scheduleFilterUserList(...args);
|
||
const refreshRenderedUserBadges = (...args) => window.refreshRenderedUserBadges(...args);
|
||
|
||
// 这些函数由 composer.js 挂载到 window
|
||
const sendMessage = (...args) => window.sendMessage(...args);
|
||
const collectChatComposerState = (...args) => window.collectChatComposerState(...args);
|
||
const buildChatMessageFormData = (...args) => window.buildChatMessageFormData(...args);
|
||
const handleChatImageSelected = (...args) => window.handleChatImageSelected(...args);
|
||
const clearSelectedChatImage = (...args) => window.clearSelectedChatImage(...args);
|
||
|
||
// 这些函数由 heartbeat.js 挂载到 window
|
||
const saveExp = (...args) => window.saveExp(...args);
|
||
const leaveRoom = (...args) => window.leaveRoom(...args);
|
||
const notifyExpiredLeave = (...args) => window.notifyExpiredLeave(...args);
|
||
|
||
// 以下函数由 preferences-status.js / message-utils.js / admin-commands.js 挂载
|
||
const toggleBlockMenu = (...args) => window.toggleBlockMenu?.(...args);
|
||
const toggleFeatureMenu = (...args) => window.toggleFeatureMenu?.(...args);
|
||
const closeFeatureMenu = (...args) => window.closeFeatureMenu?.(...args);
|
||
const openDailyStatusEditor = (...args) => window.openDailyStatusEditor?.(...args);
|
||
const closeDailyStatusEditor = (...args) => window.closeDailyStatusEditor?.(...args);
|
||
const toggleBlockedSystemSender = (...args) => window.toggleBlockedSystemSender?.(...args);
|
||
const updateDailyStatus = (...args) => window.updateDailyStatus?.(...args);
|
||
const clearDailyStatus = (...args) => window.clearDailyStatus?.(...args);
|
||
const handleFeatureLocalClear = (...args) => window.handleFeatureLocalClear?.(...args);
|
||
const normalizeChatPreferences = (...args) => window.ChatRoomTools?.normalizeChatPreferences?.(...args);
|
||
const parseDailyStatusExpiry = (...args) => window.ChatRoomTools?.parseDailyStatusExpiry?.(...args);
|
||
const normalizeDailyStatus = (...args) => window.ChatRoomTools?.normalizeDailyStatus?.(...args);
|
||
const syncDailyStatusUi = (...args) => window.syncDailyStatusUi?.() || window.ChatRoomTools?.syncDailyStatusUi?.();
|
||
const isSoundMuted = (...args) => window.ChatRoomTools?.isSoundMuted?.(...args) || false;
|
||
const toggleSoundMute = (...args) => window.toggleSoundMute?.(...args) || window.ChatRoomTools?.toggleSoundMute?.(...args);
|
||
const toggleAutoScroll = (...args) => window.ChatRoomTools?.toggleAutoScroll?.(...args);
|
||
const localClearScreen = (...args) => window.localClearScreen?.(...args);
|
||
const scrollToBottom = (...args) => window.ChatRoomTools?.scrollChatToBottom?.(...args);
|
||
|
||
// ── 挂载供其他 Blade partials 引用的函数 ────────────
|
||
window.toggleBlockMenu = toggleBlockMenu;
|
||
window.toggleFeatureMenu = toggleFeatureMenu;
|
||
window.closeFeatureMenu = closeFeatureMenu;
|
||
window.openDailyStatusEditor = openDailyStatusEditor;
|
||
window.closeDailyStatusEditor = closeDailyStatusEditor;
|
||
window.toggleBlockedSystemSender = toggleBlockedSystemSender;
|
||
window.updateDailyStatus = updateDailyStatus;
|
||
window.clearDailyStatus = clearDailyStatus;
|
||
window.handleFeatureLocalClear = handleFeatureLocalClear;
|
||
window.toggleSoundMute = toggleSoundMute;
|
||
|
||
// 同步每日状态 UI
|
||
if (typeof window.syncDailyStatusUi === 'function') {
|
||
window.syncDailyStatusUi();
|
||
}
|
||
|
||
// 绑定音效禁音控件
|
||
window.ChatRoomTools?.bindSoundMuteControl?.(() => window.saveChatPreferences?.());
|
||
|
||
</script>
|