新增聊天室状态与功能快捷菜单
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user