feat: 聊天室手机端自适应
- 新增 mobile-drawer.blade.php:手机端浮动按钮 + 工具菜单抽屉 + 名单抽屉(独立维护) - frame.blade.php:手机端代码改为 @include 引入 - chat.css:添加 @media (max-width: 640px) 响应式样式 - 隐藏桌面端工具条和右侧名单面板 - 浮动按钮样式(位于屏幕中间偏右) - 抽屉组件从顶部向下展开 - 手机端隐藏房间介绍、输入栏动作/字色/字号/禁音/分屏控件 - 现有 modal 弹窗 max-width 自适应修复 - scripts.blade.php:重构 renderUserList 提取 _renderUserListToContainer - 修复代码损坏残留,补回 setAction/scrollToBottom/autoScrollEl
This commit is contained in:
@@ -128,7 +128,13 @@
|
||||
@include('chat.partials.layout.right-panel')
|
||||
</div>
|
||||
|
||||
{{-- ═══════════ 手机端浮动按钮 + 抽屉(≤ 640px 屏幕有效,独立维护)═══════════ --}}
|
||||
@include('chat.partials.layout.mobile-drawer')
|
||||
|
||||
|
||||
|
||||
{{-- ═══════════ 全局 UI 公共组件 ═══════════ --}}
|
||||
|
||||
{{-- 自定义弹窗(替代原生 alert/confirm/prompt,全页面可用) --}}
|
||||
@include('chat.partials.global-dialog')
|
||||
{{-- Toast 轻提示 --}}
|
||||
|
||||
267
resources/views/chat/partials/layout/mobile-drawer.blade.php
Normal file
267
resources/views/chat/partials/layout/mobile-drawer.blade.php
Normal file
@@ -0,0 +1,267 @@
|
||||
{{--
|
||||
文件功能:手机端自适应抽屉组件(≤ 640px 屏幕生效)
|
||||
包含:
|
||||
1. 浮动按钮组(右下角固定):🔧 工具 / 👥 名单
|
||||
2. 工具条抽屉(底部滑入,横向网格排列工具按钮)
|
||||
3. 名单/房间抽屉(底部滑入,静态 HTML 结构,JS 动态填充)
|
||||
4. 遮罩层(点击关闭)
|
||||
5. JS 控制逻辑
|
||||
|
||||
依赖:
|
||||
- chat.css @media (max-width: 640px) 中的样式定义
|
||||
- scripts.blade.php 中的 onlineUsers / _renderUserListToContainer
|
||||
|
||||
@author ChatRoom Laravel
|
||||
@version 1.1.0
|
||||
--}}
|
||||
|
||||
{{-- ── 浮动按钮组(右下角固定) ── --}}
|
||||
<div id="mobile-fabs">
|
||||
<button class="mobile-fab" id="fab-toolbar" onclick="openMobileDrawer('toolbar')" title="工具菜单">🔧</button>
|
||||
<button class="mobile-fab" id="fab-users" onclick="openMobileDrawer('users')" title="在线名单">👥</button>
|
||||
</div>
|
||||
|
||||
{{-- ── 遮罩(点击关闭当前抽屉) ── --}}
|
||||
<div id="mobile-drawer-mask" onclick="closeMobileDrawer()"></div>
|
||||
|
||||
{{-- ── 工具条抽屉(底部滑入) ── --}}
|
||||
<div id="mobile-drawer-toolbar" class="mobile-drawer">
|
||||
<div class="mobile-drawer-header">
|
||||
🔧 工具菜单
|
||||
<button class="mobile-drawer-close" onclick="closeMobileDrawer()">✕</button>
|
||||
</div>
|
||||
<div class="mobile-drawer-body">
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();openShopModal();">🛒<br>商店</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();saveExp();">💾<br>存点</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();openGameHall();">🎮<br>娱乐</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();window.chatDialog.alert('银行功能开发中,敬请期待!','开发中','#78716c');">🏦<br>银行</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();openMarriageStatusModal();">💍<br>婚姻</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();openFriendPanel();">👫<br>好友</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();openAvatarPicker();">🖼️<br>头像</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();document.getElementById('settings-modal').style.display='flex';">⚙️<br>设置</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();window.open('{{ route('feedback.index') }}','_blank');">📝<br>反馈</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();window.open('{{ route('guestbook.index') }}','_blank');">📬<br>留言</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();window.open('{{ route('guide') }}','_blank');">📖<br>规则</div>
|
||||
<div class="mobile-tool-btn" onclick="closeMobileDrawer();window.open('{{ route('leaderboard.index') }}','_blank');">🏆<br>排行</div>
|
||||
@if ($user->id === 1 || $user->activePosition()->exists())
|
||||
<div class="mobile-tool-btn" style="color:#ffcc00;" onclick="closeMobileDrawer();window.open('/admin','_blank');">🛡️<br>管理</div>
|
||||
@endif
|
||||
<div class="mobile-tool-btn" style="color:#ffaaaa;"
|
||||
onclick="closeMobileDrawer();window.chatDialog.confirm('确定要离开聊天室吗?','离开聊天室').then(ok=>{if(ok)leaveRoom();});">🚪<br>离开</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── 名单/房间抽屉(静态 HTML,JS 动态填充) ── --}}
|
||||
<div id="mobile-drawer-users" class="mobile-drawer">
|
||||
<div class="mobile-drawer-header">
|
||||
<span id="mob-drawer-title">👥 在线名单</span>
|
||||
<button class="mobile-drawer-close" onclick="closeMobileDrawer()">✕</button>
|
||||
</div>
|
||||
|
||||
{{-- Tab 切换条 --}}
|
||||
<div style="display:flex;background:#e8f0f8;border-bottom:1px solid #b0c8e0;flex-shrink:0;">
|
||||
<button id="mob-tab-users"
|
||||
style="flex:1;padding:7px 4px;font-size:12px;border:none;background:#336699;color:#fff;cursor:pointer;font-weight:bold;"
|
||||
onclick="switchMobileTab('users')">👥 名单</button>
|
||||
<button id="mob-tab-rooms"
|
||||
style="flex:1;padding:7px 4px;font-size:12px;border:none;background:transparent;color:#336699;cursor:pointer;"
|
||||
onclick="switchMobileTab('rooms')">🏠 房间</button>
|
||||
</div>
|
||||
|
||||
{{-- 名单面板 --}}
|
||||
<div id="mob-panel-users" style="display:flex;flex-direction:column;flex:1;overflow:hidden;min-height:0;">
|
||||
{{-- 排序/搜索工具栏 --}}
|
||||
<div style="display:flex;gap:4px;padding:5px 6px;background:#f0f4f8;border-bottom:1px solid #dde8f0;flex-shrink:0;">
|
||||
<select id="mob-user-sort-select"
|
||||
style="flex:1;font-size:11px;border:1px solid #b0c8e0;border-radius:3px;padding:2px 4px;color:#224466;"
|
||||
onchange="renderMobileUserList()">
|
||||
<option value="default">默认顺序</option>
|
||||
<option value="level">按等级排</option>
|
||||
<option value="name">按名字排</option>
|
||||
</select>
|
||||
<input id="mob-user-search-input" type="text" placeholder="搜索用户..."
|
||||
style="flex:2;font-size:11px;border:1px solid #b0c8e0;border-radius:3px;padding:2px 6px;color:#333;"
|
||||
oninput="renderMobileUserList()">
|
||||
</div>
|
||||
|
||||
{{-- 用户列表容器 --}}
|
||||
<div id="mob-online-users-list"
|
||||
style="flex:1;overflow-y:auto;padding:2px 0;">
|
||||
<div style="text-align:center;color:#aaa;padding:20px;font-size:12px;">加载中...</div>
|
||||
</div>
|
||||
|
||||
{{-- 在线人数底栏 --}}
|
||||
<div id="mob-online-stats"
|
||||
style="padding:4px 8px;font-size:10px;color:#669;text-align:right;background:#f0f4f8;border-top:1px solid #dde8f0;flex-shrink:0;">
|
||||
在线: <span id="mob-online-count-footer">0</span> 人
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 房间面板 --}}
|
||||
<div id="mob-panel-rooms" style="display:none;flex:1;overflow-y:auto;min-height:0;">
|
||||
<div id="mob-rooms-online-list" style="padding:4px 0;">
|
||||
<div style="text-align:center;color:#aaa;padding:20px;font-size:12px;">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── 手机端抽屉控制脚本 ── --}}
|
||||
<script>
|
||||
/**
|
||||
* 当前打开的抽屉名称:'toolbar' | 'users' | null
|
||||
*
|
||||
* @type {string|null}
|
||||
*/
|
||||
let _mobileDrawerOpen = null;
|
||||
|
||||
/**
|
||||
* 打开指定抽屉
|
||||
*
|
||||
* @param {'toolbar'|'users'} which
|
||||
*/
|
||||
function openMobileDrawer(which) {
|
||||
// 先关掉已有的抽屉(不重置状态,避免中间态闪烁)
|
||||
if (_mobileDrawerOpen && _mobileDrawerOpen !== which) {
|
||||
document.getElementById('mobile-drawer-' + _mobileDrawerOpen)?.classList.remove('open');
|
||||
}
|
||||
|
||||
const drawer = document.getElementById('mobile-drawer-' + which);
|
||||
const mask = document.getElementById('mobile-drawer-mask');
|
||||
if (!drawer || !mask) return;
|
||||
|
||||
drawer.classList.add('open');
|
||||
mask.classList.add('open');
|
||||
_mobileDrawerOpen = which;
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 名单抽屉打开时立即渲染用户列表
|
||||
if (which === 'users') {
|
||||
renderMobileUserList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前打开的抽屉
|
||||
*
|
||||
* @param {boolean} [resetOverflow=true] 是否恢复 body overflow
|
||||
*/
|
||||
function closeMobileDrawer(resetOverflow = true) {
|
||||
if (_mobileDrawerOpen) {
|
||||
document.getElementById('mobile-drawer-' + _mobileDrawerOpen)?.classList.remove('open');
|
||||
_mobileDrawerOpen = null;
|
||||
}
|
||||
document.getElementById('mobile-drawer-mask')?.classList.remove('open');
|
||||
if (resetOverflow) {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换名单抽屉内的 Tab
|
||||
*
|
||||
* @param {'users'|'rooms'} tab
|
||||
*/
|
||||
function switchMobileTab(tab) {
|
||||
const panelUsers = document.getElementById('mob-panel-users');
|
||||
const panelRooms = document.getElementById('mob-panel-rooms');
|
||||
const tabUsers = document.getElementById('mob-tab-users');
|
||||
const tabRooms = document.getElementById('mob-tab-rooms');
|
||||
|
||||
if (panelUsers) panelUsers.style.display = tab === 'users' ? 'flex' : 'none';
|
||||
if (panelRooms) panelRooms.style.display = tab === 'rooms' ? 'block' : 'none';
|
||||
|
||||
if (tabUsers) {
|
||||
tabUsers.style.background = tab === 'users' ? '#336699' : 'transparent';
|
||||
tabUsers.style.color = tab === 'users' ? '#fff' : '#336699';
|
||||
tabUsers.style.fontWeight = tab === 'users' ? 'bold' : 'normal';
|
||||
}
|
||||
if (tabRooms) {
|
||||
tabRooms.style.background = tab === 'rooms' ? '#336699' : 'transparent';
|
||||
tabRooms.style.color = tab === 'rooms' ? '#fff' : '#336699';
|
||||
tabRooms.style.fontWeight = tab === 'rooms' ? 'bold' : 'normal';
|
||||
}
|
||||
|
||||
if (tab === 'rooms') {
|
||||
_loadMobileRoomList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将在线用户渲染到手机端名单容器
|
||||
*/
|
||||
function renderMobileUserList() {
|
||||
if (typeof onlineUsers === 'undefined') return;
|
||||
|
||||
const sortVal = document.getElementById('mob-user-sort-select')?.value || 'default';
|
||||
const keyword = (document.getElementById('mob-user-search-input')?.value || '').trim().toLowerCase();
|
||||
const container = document.getElementById('mob-online-users-list');
|
||||
if (!container) return;
|
||||
|
||||
if (typeof _renderUserListToContainer === 'function') {
|
||||
_renderUserListToContainer(container, sortVal, keyword);
|
||||
} else {
|
||||
// 降级:简单渲染用户名列表
|
||||
const users = Object.keys(onlineUsers);
|
||||
container.innerHTML = users.length
|
||||
? users.filter(u => !keyword || u.toLowerCase().includes(keyword))
|
||||
.map(u => `<div class="user-item" style="padding:5px 8px;font-size:12px;border-bottom:1px solid #eee;">${u}</div>`).join('')
|
||||
: '<div style="text-align:center;color:#aaa;padding:20px;font-size:12px;">暂无用户</div>';
|
||||
}
|
||||
|
||||
// 更新在线人数
|
||||
const count = Object.keys(onlineUsers).length;
|
||||
const footerEl = document.getElementById('mob-online-count-footer');
|
||||
if (footerEl) footerEl.textContent = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取房间列表并渲染到手机端房间容器
|
||||
*/
|
||||
function _loadMobileRoomList() {
|
||||
const container = document.getElementById('mob-rooms-online-list');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div style="text-align:center;color:#aaa;padding:16px;font-size:11px;">加载中...</div>';
|
||||
|
||||
fetch('{{ route('chat.rooms-online-status') }}')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.rooms || !data.rooms.length) {
|
||||
container.innerHTML = '<div style="text-align:center;color:#bbb;padding:16px;font-size:11px;">暂无房间</div>';
|
||||
return;
|
||||
}
|
||||
const currentRoomId = window.chatContext?.roomId;
|
||||
container.innerHTML = data.rooms.map(room => {
|
||||
const isCurrent = room.id === currentRoomId;
|
||||
const bg = isCurrent ? '#ecf4ff' : '#fff';
|
||||
const nameColor = isCurrent ? '#336699' : (room.door_open ? '#444' : '#bbb');
|
||||
const badge = room.online > 0
|
||||
? `<span style="background:#e8f5e9;color:#2e7d32;border-radius:8px;padding:0 6px;font-size:10px;font-weight:bold;">${room.online}人</span>`
|
||||
: `<span style="background:#f5f5f5;color:#bbb;border-radius:8px;padding:0 6px;font-size:10px;">空</span>`;
|
||||
const currentTag = isCurrent ? `<span style="font-size:9px;color:#7090b0;margin-left:3px;">当前</span>` : '';
|
||||
const clickAttr = isCurrent ? '' : `onclick="location.href='/room/${room.id}'"`;
|
||||
|
||||
return `<div ${clickAttr}
|
||||
style="display:flex;align-items:center;justify-content:space-between;
|
||||
padding:6px 10px;border-bottom:1px solid #eef2f8;background:${bg};
|
||||
cursor:${isCurrent ? 'default' : 'pointer'};">
|
||||
<span style="color:${nameColor};font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;margin-right:6px;">
|
||||
${room.name}${currentTag}
|
||||
</span>${badge}
|
||||
</div>`;
|
||||
}).join('');
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML = '<div style="text-align:center;color:#f00;padding:10px;font-size:11px;">加载失败</div>';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听全局用户列表更新,若名单抽屉已打开则同步刷新
|
||||
*/
|
||||
window.addEventListener('chatroom:users-updated', () => {
|
||||
if (_mobileDrawerOpen === 'users') {
|
||||
renderMobileUserList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -119,8 +119,12 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── 动作选择 ──────────────────────────────────────
|
||||
/**
|
||||
* 设置发言动作并聚焦输入框
|
||||
*
|
||||
* @param {string} act 动作名称
|
||||
*/
|
||||
function setAction(act) {
|
||||
document.getElementById('action').value = act;
|
||||
switchTab('users');
|
||||
@@ -136,6 +140,9 @@
|
||||
}
|
||||
|
||||
// ── 滚动到底部 ───────────────────────────────────
|
||||
/**
|
||||
* 将公聊窗口滚动到最新消息(受 autoScroll 开关控制)
|
||||
*/
|
||||
function scrollToBottom() {
|
||||
if (autoScroll) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
@@ -143,9 +150,16 @@
|
||||
}
|
||||
|
||||
// ── 渲染在线人员列表(支持排序) ──────────────────
|
||||
function renderUserList() {
|
||||
userList.innerHTML = '';
|
||||
toUserSelect.innerHTML = '<option value="大家">大家</option>';
|
||||
/**
|
||||
* 核心渲染函数:将在线用户渲染到指定容器(桌面端名单区和手机端抽屉共用)
|
||||
*
|
||||
* @param {HTMLElement} targetContainer 目标 DOM 容器
|
||||
* @param {string} sortBy 排序方式:'default' | 'name' | 'level'
|
||||
* @param {string} keyword 搜索关键词(小写)
|
||||
*/
|
||||
function _renderUserListToContainer(targetContainer, sortBy, keyword) {
|
||||
if (!targetContainer) return;
|
||||
targetContainer.innerHTML = '';
|
||||
|
||||
// 在列表顶部添加"大家"条目(原版风格)
|
||||
let allDiv = document.createElement('div');
|
||||
@@ -154,7 +168,7 @@
|
||||
allDiv.onclick = () => {
|
||||
toUserSelect.value = '大家';
|
||||
};
|
||||
userList.appendChild(allDiv);
|
||||
targetContainer.appendChild(allDiv);
|
||||
|
||||
// ── AI 小助手(仅当全局开关开启时显示,与普通用户风格一致)──
|
||||
if (window.chatContext.chatBotEnabled) {
|
||||
@@ -168,19 +182,9 @@
|
||||
toUserSelect.value = 'AI小班长';
|
||||
document.getElementById('content').focus();
|
||||
};
|
||||
userList.appendChild(botDiv);
|
||||
|
||||
// 在发言对象下拉框中也添加 AI 小助手
|
||||
let botOption = document.createElement('option');
|
||||
botOption.value = 'AI小班长';
|
||||
botOption.textContent = '🤖 AI小班长';
|
||||
toUserSelect.appendChild(botOption);
|
||||
targetContainer.appendChild(botDiv);
|
||||
}
|
||||
|
||||
// 获取排序方式
|
||||
const sortSelect = document.getElementById('user-sort-select');
|
||||
const sortBy = sortSelect ? sortSelect.value : 'default';
|
||||
|
||||
// 构建用户数组并排序
|
||||
let userArr = [];
|
||||
for (let username in onlineUsers) {
|
||||
@@ -196,21 +200,23 @@
|
||||
userArr.sort((a, b) => (b.user_level || 0) - (a.user_level || 0));
|
||||
}
|
||||
|
||||
let count = userArr.length;
|
||||
|
||||
userArr.forEach(user => {
|
||||
const username = user.username;
|
||||
|
||||
// 搜索过滤
|
||||
if (keyword && !username.toLowerCase().includes(keyword)) return;
|
||||
|
||||
let item = document.createElement('div');
|
||||
item.className = 'user-item';
|
||||
item.dataset.username = username;
|
||||
|
||||
const headface = (user.headface || '1.gif').toLowerCase();
|
||||
const headImgSrc = headface.startsWith('storage/') ? '/' + headface : '/images/headface/' + headface;
|
||||
const headImgSrc = headface.startsWith('storage/') ? '/' + headface : '/images/headface/' +
|
||||
headface;
|
||||
|
||||
// 徽章优先级:职务图标 > 管理员 > VIP
|
||||
let badges = '';
|
||||
if (user.position_icon) {
|
||||
// 有职务:显示职务图标,hover 显示职务名称
|
||||
const posTitle = (user.position_name || '在职') + ' · ' + username;
|
||||
badges +=
|
||||
`<span style="font-size:13px; margin-left:2px;" title="${posTitle}">${user.position_icon}</span>`;
|
||||
@@ -235,23 +241,48 @@
|
||||
};
|
||||
// 双击打开用户名片弹窗(全局统一入口)
|
||||
item.ondblclick = () => openUserCard(username);
|
||||
userList.appendChild(item);
|
||||
targetContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function renderUserList() {
|
||||
userList.innerHTML = '';
|
||||
toUserSelect.innerHTML = '<option value="大家">大家</option>';
|
||||
|
||||
// 获取排序方式和搜索词
|
||||
const sortSelect = document.getElementById('user-sort-select');
|
||||
const sortBy = sortSelect ? sortSelect.value : 'default';
|
||||
const searchInput = document.getElementById('user-search-input');
|
||||
const keyword = searchInput ? searchInput.value.trim().toLowerCase() : '';
|
||||
|
||||
// 调用核心渲染(桌面端名单容器)
|
||||
_renderUserListToContainer(userList, sortBy, keyword);
|
||||
|
||||
// 重新填充发言对象下拉框(不过滤关键词,始终显示全部用户)
|
||||
toUserSelect.innerHTML = '<option value="大家">大家</option>';
|
||||
if (window.chatContext.chatBotEnabled) {
|
||||
let botOption = document.createElement('option');
|
||||
botOption.value = 'AI小班长';
|
||||
botOption.textContent = '🤖 AI小班长';
|
||||
toUserSelect.appendChild(botOption);
|
||||
}
|
||||
for (let username in onlineUsers) {
|
||||
if (username !== window.chatContext.username) {
|
||||
let option = document.createElement('option');
|
||||
option.value = username;
|
||||
option.textContent = username;
|
||||
toUserSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const count = Object.keys(onlineUsers).length;
|
||||
onlineCount.innerText = count;
|
||||
onlineCountBottom.innerText = count;
|
||||
const footer = document.getElementById('online-count-footer');
|
||||
if (footer) footer.innerText = count;
|
||||
if (footer) { footer.innerText = count; }
|
||||
|
||||
// 如果有搜索关键词,重新过滤
|
||||
filterUserList();
|
||||
// 派发用户列表更新事件,供手机端抽屉同步
|
||||
window.dispatchEvent(new Event('chatroom:users-updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,6 +306,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 追加消息到聊天窗格(原版风格:非气泡模式,逐行显示)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user