新增聊天室状态与功能快捷菜单

This commit is contained in:
2026-04-24 21:17:44 +08:00
parent d7ec42a025
commit 0f0bfef2a8
18 changed files with 1361 additions and 124 deletions
+5 -14
View File
@@ -57,20 +57,8 @@
if ($chatbotEnabledState) {
$botUser = \App\Models\User::where('username', 'AI小班长')->first();
if ($botUser) {
$botUserData = [
'user_id' => $botUser->id,
'username' => $botUser->username,
'level' => $botUser->user_level,
'sex' => $botUser->sex,
'headface' => $botUser->headface,
'headfaceUrl' => $botUser->headfaceUrl,
'vip_icon' => $botUser->vipIcon(),
'vip_name' => $botUser->vipName(),
'vip_color' => $botUser->isVip() ? ($botUser->vipLevel?->color ?? '') : '',
'is_admin' => false,
'position_icon' => '',
'position_name' => '',
];
$botUserData = app(\App\Services\ChatUserPresenceService::class)->build($botUser);
$botUserData['headfaceUrl'] = $botUser->headfaceUrl;
}
}
@endphp
@@ -106,9 +94,12 @@
rewardQuotaUrl: "{{ route('command.reward_quota') }}",
refreshAllUrl: "{{ route('command.refresh_all') }}",
chatPreferencesUrl: "{{ route('user.update_chat_preferences') }}",
dailyStatusUpdateUrl: "{{ route('user.update_daily_status') }}",
userJjb: {{ (int) $user->jjb }}, // 当前用户金币(求婚前金额预检查用)
myGold: {{ (int) $user->jjb }}, // 赠金币面板显示余额用(赠送成功后前端更新)
chatPreferences: @json($user->chat_preferences ?? []),
currentDailyStatus: @json($activeDailyStatus),
dailyStatusCatalog: @json($dailyStatusCatalog),
// ─── 婚姻系统 ──────────────────────────────
minWeddingCost: {{ (int) \App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->value('amount') ?? 0 }},
@@ -15,6 +15,7 @@
$canSendRedPacket = $roomPermissionMap[\App\Support\PositionPermissionRegistry::ROOM_RED_PACKET] ?? false;
$canManageLossCover = $roomPermissionMap[\App\Support\PositionPermissionRegistry::ROOM_BACCARAT_LOSS_COVER] ?? false;
$canTriggerFullscreenEffect = $roomPermissionMap[\App\Support\PositionPermissionRegistry::ROOM_FULLSCREEN_EFFECT] ?? false;
$currentDailyStatusKey = $activeDailyStatus['key'] ?? '';
@endphp
<div class="input-bar">
@@ -165,6 +166,94 @@ $welcomeMessages = [
</div>
</div>
<div style="position:relative;display:inline-block;" id="feature-btn-wrap">
<button type="button" onclick="toggleFeatureMenu(event)"
style="font-size:11px;padding:1px 6px;background:linear-gradient(135deg,#4f46e5,#6366f1);color:#fff;border:none;border-radius:2px;cursor:pointer;font-weight:bold;">
⚙️ 功能
</button>
<div id="feature-menu"
onclick="event.stopPropagation()"
style="display:none;position:absolute;bottom:calc(100% + 6px);left:0;z-index:10020;min-width:236px;padding:10px;background:#eef2ff;border:1px solid #c7d2fe;border-radius:10px;box-shadow:0 10px 24px rgba(15,23,42,.18);">
<div style="font-size:10px;color:#4338ca;padding:0 2px 8px;">常用操作</div>
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;">
<button type="button" onclick="handleFeatureLocalClear()"
style="font-size:11px;padding:6px 8px;background:#fff;color:#475569;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">🧹 清屏</button>
<button type="button" onclick="openDailyStatusEditor()"
style="font-size:11px;padding:6px 8px;background:#fff;color:#4f46e5;border:1px solid #a5b4fc;border-radius:6px;cursor:pointer;">
<span id="daily-status-shortcut-icon">{{ $activeDailyStatus['icon'] ?? '🙂' }}</span>
<span id="daily-status-shortcut-label">{{ $activeDailyStatus['label'] ?? '状态' }}</span>
</button>
</div>
<div style="font-size:10px;color:#4338ca;padding:10px 2px 8px;">快捷入口</div>
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;">
<button type="button" onclick="runFeatureShortcut('shop')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">🛍 商店</button>
<button type="button" onclick="runFeatureShortcut('vip')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">👑 会员</button>
<button type="button" onclick="runFeatureShortcut('game')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">🎮 娱乐</button>
<button type="button" onclick="runFeatureShortcut('avatar')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">🖼 头像</button>
<button type="button" onclick="runFeatureShortcut('bank')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">🏦 银行</button>
<button type="button" onclick="runFeatureShortcut('marriage')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">💍 婚姻</button>
<button type="button" onclick="runFeatureShortcut('friend')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">👥 好友</button>
<button type="button" onclick="runFeatureShortcut('settings')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">⚙️ 设置</button>
</div>
</div>
</div>
<div id="daily-status-editor-overlay"
onclick="closeDailyStatusEditor()"
style="display:none;position:fixed;inset:0;z-index:10030;background:rgba(15,23,42,.45);backdrop-filter:blur(2px);">
<div id="daily-status-editor"
onclick="event.stopPropagation()"
style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(92vw,620px);max-height:min(78vh,680px);overflow-y:auto;padding:14px;background:linear-gradient(180deg,#eef2ff 0%,#f8fafc 100%);border:1px solid #c7d2fe;border-radius:14px;box-shadow:0 18px 40px rgba(15,23,42,.28);">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:4px 4px 12px;">
<div>
<div style="font-size:22px;font-weight:bold;color:#312e81;line-height:1.2;">设个状态</div>
<div style="font-size:12px;color:#6366f1;margin-top:4px;">朋友当天内可见,次日会自动失效</div>
</div>
<button type="button" onclick="closeDailyStatusEditor()"
style="width:28px;height:28px;border:none;border-radius:999px;background:#e0e7ff;color:#4338ca;font-size:18px;cursor:pointer;line-height:1;">×</button>
</div>
<div
style="display:flex;flex-wrap:wrap;gap:8px;padding:0 4px 12px;margin-bottom:12px;border-bottom:1px solid #dbeafe;">
<button type="button" onclick="clearDailyStatus()"
style="font-size:12px;padding:7px 12px;background:#fff7ed;color:#c2410c;border:1px solid #fdba74;border-radius:999px;cursor:pointer;">
♻️ 清除状态
</button>
</div>
@foreach ($dailyStatusCatalog as $group)
<div style="padding:0 4px 14px;">
<div style="font-size:15px;font-weight:bold;color:#475569;margin-bottom:10px;">
{{ $group['group'] }}
</div>
<div style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;">
@foreach ($group['items'] as $item)
<button type="button"
class="daily-status-item"
data-status-key="{{ $item['key'] }}"
data-status-label="{{ $item['label'] }}"
data-status-icon="{{ $item['icon'] }}"
data-status-active="{{ $currentDailyStatusKey === $item['key'] ? '1' : '0' }}"
onclick="updateDailyStatus('{{ $item['key'] }}')"
style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:82px;padding:10px 6px;background:#ffffffcc;border:1px solid #e5e7eb;border-radius:12px;cursor:pointer;transition:all .15s ease;color:#334155;">
<span style="font-size:24px;line-height:1;">{{ $item['icon'] }}</span>
<span style="font-size:12px;line-height:1.25;text-align:center;">{{ $item['label'] }}</span>
</button>
@endforeach
</div>
</div>
@endforeach
</div>
</div>
@if (! empty($hasRoomManagementPermission))
<div style="position:relative;display:inline-block;" id="admin-btn-wrap">
<button type="button" onclick="toggleAdminMenu(event)"
@@ -235,10 +324,6 @@ $welcomeMessages = [
</div>
</div>
@endif
<button type="button" onclick="localClearScreen()"
style="font-size: 11px; padding: 1px 6px; background: #64748b; color: #fff; border: none; border-radius: 2px; cursor: pointer;">🔄
清屏</button>
</div>
{{-- 第二行:输入框 + 发送 --}}
@@ -24,7 +24,7 @@
<div class="tool-btn" onclick="openMarriageStatusModal()" title="婚姻状态">婚姻</div>
<div class="tool-btn" onclick="openFriendPanel()" title="好友列表">好友</div>
<div class="tool-btn" onclick="openAvatarPicker()" title="修改头像">头像</div>
<div class="tool-btn" onclick="document.getElementById('settings-modal').style.display='flex'" title="个人设置">设置
<div class="tool-btn" onclick="openSettingsModal()" title="个人设置">设置
</div>
<div class="tool-btn" onclick="window.open('{{ route('feedback.index') }}', '_blank')" title="反馈">反馈</div>
<div class="tool-btn" onclick="window.open('{{ route('guestbook.index') }}', '_blank')" title="留言板/私信">留言</div>
@@ -89,9 +89,10 @@
{{-- ═══════════ 个人设置弹窗 ═══════════ --}}
<div id="settings-modal"
onclick="closeSettingsModal()"
style="display:none; position:fixed; top:0; left:0; right:0; bottom:0;
background:rgba(0,0,0,0.5); z-index:10000; justify-content:center; align-items:center;">
<div
<div onclick="event.stopPropagation()"
style="background:#fff; border-radius:8px; width:380px; max-height:90vh;
box-shadow:0 8px 32px rgba(0,0,0,0.3); display:flex; flex-direction:column;">
{{-- --}}
@@ -99,7 +100,7 @@
style="background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff;
padding:12px 16px; border-radius:8px 8px 0 0; display:flex; justify-content:space-between; align-items:center; flex-shrink:0;">
<span style="font-size:14px; font-weight:bold;">⚙️ 个人设置</span>
<span onclick="document.getElementById('settings-modal').style.display='none'"
<span onclick="closeSettingsModal()"
style="cursor:pointer; font-size:18px; opacity:0.8;">&times;</span>
</div>
@@ -261,6 +262,20 @@
document.getElementById('avatar-picker-modal').style.display = 'none';
}
/**
* 打开个人设置弹窗。
*/
function openSettingsModal() {
document.getElementById('settings-modal').style.display = 'flex';
}
/**
* 关闭个人设置弹窗。
*/
function closeSettingsModal() {
document.getElementById('settings-modal').style.display = 'none';
}
/**
* 加载头像列表(懒加载,首次打开时请求)
*/
+580 -24
View File
@@ -72,6 +72,7 @@
})();
let onlineUsers = {};
let autoScroll = true;
let userBadgeRotationTick = 0;
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
@@ -94,6 +95,394 @@
};
}
/**
* 解析并标准化状态到期时间。
*
* @param {string|null|undefined} expiresAt 原始到期时间
* @returns {Date|null}
*/
function parseDailyStatusExpiry(expiresAt) {
if (!expiresAt) {
return null;
}
const parsed = new Date(expiresAt);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
/**
* 将状态对象规整为前端统一结构,并过滤掉已过期状态。
*
* @param {Record<string, any>|null|undefined} raw 原始状态对象
* @returns {Object|null}
*/
function normalizeDailyStatus(raw) {
if (!raw || typeof raw !== 'object') {
return null;
}
const key = String(raw.key ?? raw.daily_status_key ?? '');
const label = String(raw.label ?? raw.daily_status_label ?? '');
const icon = String(raw.icon ?? raw.daily_status_icon ?? '');
const group = String(raw.group ?? raw.daily_status_group ?? '');
const expiresAt = raw.expires_at ?? raw.daily_status_expires_at ?? null;
const parsedExpiry = parseDailyStatusExpiry(expiresAt);
if (!key || !label || !icon || !parsedExpiry) {
return null;
}
if (parsedExpiry.getTime() <= Date.now()) {
return null;
}
return {
key,
label,
icon,
group,
expires_at: parsedExpiry.toISOString(),
};
}
/**
* 获取当前登录用户仍然有效的状态。
*
* @returns {Object|null}
*/
function getCurrentUserDailyStatus() {
return normalizeDailyStatus(window.chatContext?.currentDailyStatus);
}
/**
* 清除用户在线载荷中的状态字段,避免合并时残留旧状态。
*
* @param {Record<string, any>} payload 用户在线载荷
*/
function removeDailyStatusFields(payload) {
if (!payload || typeof payload !== 'object') {
return;
}
delete payload.daily_status_key;
delete payload.daily_status_label;
delete payload.daily_status_icon;
delete payload.daily_status_group;
delete payload.daily_status_expires_at;
}
/**
* 将状态写回指定用户的在线载荷。
*
* @param {string} username 用户名
* @param {Object|null} status 标准化后的状态对象
*/
function setOnlineUserDailyStatus(username, status) {
if (!username || !onlineUsers[username]) {
return;
}
removeDailyStatusFields(onlineUsers[username]);
if (!status) {
return;
}
onlineUsers[username].daily_status_key = status.key;
onlineUsers[username].daily_status_label = status.label;
onlineUsers[username].daily_status_icon = status.icon;
onlineUsers[username].daily_status_group = status.group;
onlineUsers[username].daily_status_expires_at = status.expires_at;
}
/**
* 用服务端最新的在线载荷刷新指定用户,并先清空旧状态字段。
*
* @param {string} username 用户名
* @param {Record<string, any>} payload 最新在线载荷
*/
function hydrateOnlineUserPayload(username, payload) {
const nextPayload = {
...(onlineUsers[username] || {}),
};
removeDailyStatusFields(nextPayload);
onlineUsers[username] = {
...nextPayload,
...payload,
};
}
/**
* 同步状态按钮文字与图标。
*/
function syncDailyStatusTrigger() {
const shortcutIcon = document.getElementById('daily-status-shortcut-icon');
const shortcutLabel = document.getElementById('daily-status-shortcut-label');
const activeStatus = getCurrentUserDailyStatus();
if (shortcutIcon) {
shortcutIcon.textContent = activeStatus?.icon || '🙂';
}
if (shortcutLabel) {
shortcutLabel.textContent = activeStatus?.label || '状态';
}
}
/**
* 同步状态面板中当前选中项的高亮样式。
*/
function syncDailyStatusMenuSelection() {
const activeKey = getCurrentUserDailyStatus()?.key || '';
document.querySelectorAll('#daily-status-editor-overlay .daily-status-item').forEach((button) => {
const selected = button.dataset.statusKey === activeKey;
button.style.borderColor = selected ? '#6366f1' : '#e5e7eb';
button.style.background = selected ? 'linear-gradient(180deg,#eef2ff 0%,#e0e7ff 100%)' : '#ffffffcc';
button.style.color = selected ? '#312e81' : '#334155';
button.style.boxShadow = selected ? '0 8px 18px rgba(99,102,241,.18)' : 'none';
button.style.transform = selected ? 'translateY(-1px)' : 'translateY(0)';
});
}
/**
* 同步聊天室状态相关 UI。
*/
function syncDailyStatusUi() {
const activeStatus = getCurrentUserDailyStatus();
if (window.chatContext) {
window.chatContext.currentDailyStatus = activeStatus;
}
syncDailyStatusTrigger();
syncDailyStatusMenuSelection();
}
/**
* 关闭功能菜单。
*/
function closeFeatureMenu() {
const menu = document.getElementById('feature-menu');
if (menu) {
menu.style.display = 'none';
}
}
/**
* 切换功能菜单显示状态。
*
* @param {Event} event 点击事件
*/
function toggleFeatureMenu(event) {
event.stopPropagation();
const menu = document.getElementById('feature-menu');
const welcomeMenu = document.getElementById('welcome-menu');
const adminMenu = document.getElementById('admin-menu');
const blockMenu = document.getElementById('block-menu');
const editorOverlay = document.getElementById('daily-status-editor-overlay');
if (!menu) {
return;
}
if (welcomeMenu) {
welcomeMenu.style.display = 'none';
}
if (adminMenu) {
adminMenu.style.display = 'none';
}
if (blockMenu) {
blockMenu.style.display = 'none';
}
if (editorOverlay) {
editorOverlay.style.display = 'none';
}
syncDailyStatusUi();
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
/**
* 打开状态编辑窗口。
*/
function openDailyStatusEditor() {
const overlay = document.getElementById('daily-status-editor-overlay');
closeFeatureMenu();
syncDailyStatusUi();
if (overlay) {
overlay.style.display = 'block';
}
}
/**
* 关闭状态编辑窗口。
*/
function closeDailyStatusEditor() {
const overlay = document.getElementById('daily-status-editor-overlay');
if (overlay) {
overlay.style.display = 'none';
}
}
/**
* 执行功能菜单中的快捷入口,并在打开目标面板前先关闭功能菜单。
*
* @param {string} action 快捷入口动作名
*/
function runFeatureShortcut(action) {
closeFeatureMenu();
if (action === 'shop' && typeof window.openShopModal === 'function') {
window.openShopModal();
return;
}
if (action === 'vip' && typeof window.openVipModal === 'function') {
window.openVipModal();
return;
}
if (action === 'game' && typeof window.openGameHall === 'function') {
window.openGameHall();
return;
}
if (action === 'avatar' && typeof window.openAvatarPicker === 'function') {
window.openAvatarPicker();
return;
}
if (action === 'bank' && typeof window.openBankModal === 'function') {
window.openBankModal();
return;
}
if (action === 'marriage' && typeof window.openMarriageStatusModal === 'function') {
window.openMarriageStatusModal();
return;
}
if (action === 'friend' && typeof window.openFriendPanel === 'function') {
window.openFriendPanel();
return;
}
if (action === 'settings' && typeof window.openSettingsModal === 'function') {
window.openSettingsModal();
}
}
/**
* 提交状态设置/清除请求。
*
* @param {Object} payload 请求载荷
* @returns {Promise<Object>}
*/
async function submitDailyStatusPayload(payload) {
const response = await fetch(window.chatContext.dailyStatusUpdateUrl, {
method: 'PUT',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
const result = await response.json();
if (!response.ok || result?.status !== 'success') {
throw new Error(result?.message || '状态保存失败');
}
return result;
}
/**
* 将服务端返回的状态结果应用到当前用户本地名单。
*
* @param {Object|null} status 标准化后的状态对象
*/
function applyCurrentUserDailyStatus(status) {
if (window.chatContext) {
window.chatContext.currentDailyStatus = status;
}
setOnlineUserDailyStatus(window.chatContext.username, status);
syncDailyStatusUi();
renderUserList();
}
/**
* 设置新的当日状态。
*
* @param {string} statusKey 状态键
*/
async function updateDailyStatus(statusKey) {
if (!window.chatContext?.dailyStatusUpdateUrl) {
return;
}
try {
const result = await submitDailyStatusPayload({
room_id: window.chatContext.roomId,
action: 'set',
status_key: statusKey,
});
const status = normalizeDailyStatus(result?.data?.status);
applyCurrentUserDailyStatus(status);
closeDailyStatusEditor();
window.chatToast?.show({
title: '状态已更新',
message: status ? `${status.icon} ${status.label}` : '已更新',
icon: status?.icon || '🙂',
color: '#4f46e5',
duration: 2600,
});
} catch (error) {
window.chatDialog?.alert(error.message || '状态设置失败', '操作失败', '#cc4444');
}
}
/**
* 清除当前当日状态。
*/
async function clearDailyStatus() {
if (!window.chatContext?.dailyStatusUpdateUrl) {
return;
}
try {
await submitDailyStatusPayload({
room_id: window.chatContext.roomId,
action: 'clear',
});
applyCurrentUserDailyStatus(null);
closeDailyStatusEditor();
window.chatToast?.show({
title: '状态已清除',
message: '名字后方将恢复默认徽标展示。',
icon: '♻️',
color: '#c2410c',
duration: 2400,
});
} catch (error) {
window.chatDialog?.alert(error.message || '状态清除失败', '操作失败', '#cc4444');
}
}
/**
* localStorage 读取已屏蔽的系统播报发送者列表。
*
@@ -308,6 +697,8 @@
const menu = document.getElementById('block-menu');
const welcomeMenu = document.getElementById('welcome-menu');
const adminMenu = document.getElementById('admin-menu');
const featureMenu = document.getElementById('feature-menu');
const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');
if (!menu) {
return;
@@ -321,6 +712,14 @@
adminMenu.style.display = 'none';
}
if (featureMenu) {
featureMenu.style.display = 'none';
}
if (dailyStatusEditor) {
dailyStatusEditor.style.display = 'none';
}
syncBlockedSystemSenderCheckboxes();
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
@@ -615,6 +1014,8 @@
const menu = document.getElementById('welcome-menu');
const adminMenu = document.getElementById('admin-menu');
const blockMenu = document.getElementById('block-menu');
const featureMenu = document.getElementById('feature-menu');
const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');
if (!menu) {
return;
}
@@ -624,6 +1025,12 @@
if (blockMenu) {
blockMenu.style.display = 'none';
}
if (featureMenu) {
featureMenu.style.display = 'none';
}
if (dailyStatusEditor) {
dailyStatusEditor.style.display = 'none';
}
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
@@ -635,6 +1042,8 @@
const menu = document.getElementById('admin-menu');
const welcomeMenu = document.getElementById('welcome-menu');
const blockMenu = document.getElementById('block-menu');
const featureMenu = document.getElementById('feature-menu');
const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');
if (!menu) {
return;
}
@@ -644,6 +1053,12 @@
if (blockMenu) {
blockMenu.style.display = 'none';
}
if (featureMenu) {
featureMenu.style.display = 'none';
}
if (dailyStatusEditor) {
dailyStatusEditor.style.display = 'none';
}
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
@@ -803,6 +1218,16 @@
if (blockMenu) {
blockMenu.style.display = 'none';
}
const featureMenu = document.getElementById('feature-menu');
if (featureMenu) {
featureMenu.style.display = 'none';
}
const dailyStatusEditor = document.getElementById('daily-status-editor-overlay');
if (dailyStatusEditor) {
dailyStatusEditor.style.display = 'none';
}
});
// ── 动作选择 ──────────────────────────────────────
@@ -986,29 +1411,14 @@
const headImgSrc = headface.startsWith('storage/') ? '/' + headface : '/images/headface/' +
headface;
// 徽章优先级:职务图标 > 管理员 > VIP
let badges = '';
if (user.position_icon) {
const posTitle = (user.position_name || '在职') + ' · ' + username;
const safePosTitle = escapeHtml(String(posTitle));
const safePositionIcon = escapeHtml(String(user.position_icon || '🎖️'));
badges +=
`<span class="user-badge-icon" style="font-size:13px; margin-left:2px;" data-instant-tooltip="${safePosTitle}">${safePositionIcon}</span>`;
} else if (user.is_admin) {
badges += `<span class="user-badge-icon" style="font-size:12px; margin-left:2px;" data-instant-tooltip="最高统帅">🎖️</span>`;
} else if (user.vip_icon) {
const vipColor = user.vip_color || '#f59e0b';
const safeVipTitle = escapeHtml(String(user.vip_name || 'VIP'));
const safeVipIcon = escapeHtml(String(user.vip_icon || '👑'));
badges +=
`<span class="user-badge-icon" style="font-size:12px; margin-left:2px; color:${vipColor};" data-instant-tooltip="${safeVipTitle}">${safeVipIcon}</span>`;
}
const badges = buildUserBadgeHtml(user, username);
// 女生名字使用玫粉色
const nameColor = (user.sex == 2) ? 'color:#e91e8c;' : '';
item.innerHTML = `
<img class="user-head" src="${headImgSrc}" onerror="this.src='/images/headface/1.gif'">
<span class="user-name" style="${nameColor}">${username}</span>${badges}
<span class="user-name" style="${nameColor}">${username}</span>
<span class="user-badge-slot">${badges}</span>
`;
// 单击/双击互斥:单击延迟 250ms 执行,双击取消单击定时器后直接执行双击逻辑
@@ -1048,6 +1458,8 @@
}, { passive: false });
targetContainer.appendChild(item);
});
refreshRenderedUserBadges(targetContainer);
}
function renderUserList() {
@@ -1089,6 +1501,115 @@
window.dispatchEvent(new Event('chatroom:users-updated'));
}
/**
* 获取用户当前仍然有效的当日状态。
*
* @param {Record<string, any>} user 用户在线载荷
* @returns {Object|null}
*/
function resolveUserDailyStatus(user) {
return normalizeDailyStatus(user);
}
/**
* 构建原有徽标(职务 / 管理员 / VIP)。
*
* @param {Record<string, any>} user 用户在线载荷
* @param {string} username 用户名
* @returns {string}
*/
function buildUserPrimaryBadgeHtml(user, username) {
if (user.position_icon) {
const posTitle = (user.position_name || '在职') + ' · ' + username;
const safePosTitle = escapeHtml(String(posTitle));
const safePositionIcon = escapeHtml(String(user.position_icon || '🎖️'));
return `<span class="user-badge-icon" style="font-size:13px; margin-left:2px;" data-instant-tooltip="${safePosTitle}">${safePositionIcon}</span>`;
}
if (user.is_admin) {
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px;" data-instant-tooltip="最高统帅">🎖️</span>`;
}
if (user.vip_icon) {
const vipColor = user.vip_color || '#f59e0b';
const safeVipTitle = escapeHtml(String(user.vip_name || 'VIP'));
const safeVipIcon = escapeHtml(String(user.vip_icon || '👑'));
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px; color:${vipColor};" data-instant-tooltip="${safeVipTitle}">${safeVipIcon}</span>`;
}
return '';
}
/**
* 构建状态徽标。
*
* @param {Record<string, any>} user 用户在线载荷
* @returns {string}
*/
function buildUserStatusBadgeHtml(user) {
const status = resolveUserDailyStatus(user);
if (!status) {
return '';
}
const safeIcon = escapeHtml(status.icon);
const safeLabel = escapeHtml(status.label);
const safeTooltip = escapeHtml(`${status.group} · ${status.label}`);
return `
<span style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#4338ca;font-size:11px;line-height:18px;vertical-align:middle;"
data-instant-tooltip="${safeTooltip}">
<span style="font-size:11px;line-height:1;">${safeIcon}</span>
<span style="line-height:1;">${safeLabel}</span>
</span>
`;
}
/**
* 3 秒节奏在原有徽标与状态徽标之间切换。
*
* @param {Record<string, any>} user 用户在线载荷
* @param {string} username 用户名
* @returns {string}
*/
function buildUserBadgeHtml(user, username) {
const statusBadge = buildUserStatusBadgeHtml(user);
const primaryBadge = buildUserPrimaryBadgeHtml(user, username);
if (statusBadge && primaryBadge) {
return userBadgeRotationTick % 2 === 0 ? statusBadge : primaryBadge;
}
return statusBadge || primaryBadge;
}
/**
* 仅刷新当前已渲染用户行的徽标槽位,避免重建头像节点造成闪烁。
*
* @param {HTMLElement|Document} scope 需要刷新的 DOM 范围
*/
function refreshRenderedUserBadges(scope = document) {
scope.querySelectorAll('.user-item[data-username]').forEach((item) => {
const username = item.dataset.username;
const badgeSlot = item.querySelector('.user-badge-slot');
if (!username || !badgeSlot) {
return;
}
badgeSlot.innerHTML = buildUserBadgeHtml(onlineUsers[username] || {}, username);
});
}
// 名单中“状态 / 原徽标”双轨展示时,每 3 秒只刷新徽标槽位,不重建头像行。
window.setInterval(() => {
userBadgeRotationTick = (userBadgeRotationTick + 1) % 2;
refreshRenderedUserBadges();
syncDailyStatusUi();
}, 3000);
/**
* 搜索/过滤用户列表
*/
@@ -1467,17 +1988,17 @@
const users = e.detail;
onlineUsers = {};
users.forEach(u => {
onlineUsers[u.username] = u;
hydrateOnlineUserPayload(u.username, u);
});
// 初始加载时,如果全局且开启,注入 AI
if (window.chatContext.chatBotEnabled && window.chatContext.botUser) {
onlineUsers['AI小班长'] = window.chatContext.botUser;
hydrateOnlineUserPayload('AI小班长', window.chatContext.botUser);
}
setOnlineUserDailyStatus(window.chatContext.username, getCurrentUserDailyStatus());
syncDailyStatusUi();
renderUserList();
});
// 监听机器人动态开关
@@ -1486,7 +2007,7 @@
window.chatContext.chatBotEnabled = detail.isOnline;
if (detail.isOnline && detail.user && detail.user.username) {
onlineUsers[detail.user.username] = detail.user;
hydrateOnlineUserPayload(detail.user.username, detail.user);
window.chatContext.botUser = detail.user;
} else {
delete onlineUsers['AI小班长'];
@@ -1495,9 +2016,27 @@
renderUserList();
});
window.addEventListener('chat:user-status-updated', (e) => {
const username = e.detail?.username;
const payload = e.detail?.user;
if (!username || !payload) {
return;
}
hydrateOnlineUserPayload(username, payload);
if (username === window.chatContext.username) {
window.chatContext.currentDailyStatus = normalizeDailyStatus(payload);
syncDailyStatusUi();
}
renderUserList();
});
window.addEventListener('chat:joining', (e) => {
const user = e.detail;
onlineUsers[user.username] = user;
hydrateOnlineUserPayload(user.username, user);
renderUserList();
});
@@ -1904,10 +2443,19 @@
}
window.toggleAdminMenu = toggleAdminMenu;
window.toggleBlockMenu = toggleBlockMenu;
window.toggleFeatureMenu = toggleFeatureMenu;
window.closeFeatureMenu = closeFeatureMenu;
window.openDailyStatusEditor = openDailyStatusEditor;
window.closeDailyStatusEditor = closeDailyStatusEditor;
window.runFeatureShortcut = runFeatureShortcut;
window.runAdminAction = runAdminAction;
window.selectEffect = selectEffect;
window.triggerEffect = triggerEffect;
window.toggleBlockedSystemSender = toggleBlockedSystemSender;
window.updateDailyStatus = updateDailyStatus;
window.clearDailyStatus = clearDailyStatus;
window.handleFeatureLocalClear = handleFeatureLocalClear;
syncDailyStatusUi();
// ── 字号设置(持久化到 localStorage)─────────────────
/**
@@ -2480,6 +3028,14 @@
}
}
/**
* 在状态面板中触发本地清屏,并顺手关闭面板。
*/
function handleFeatureLocalClear() {
closeFeatureMenu();
localClearScreen();
}
// ── 滚屏开关 ─────────────────────────────────────
function toggleAutoScroll() {
autoScroll = !autoScroll;