新增聊天室状态与功能快捷菜单
This commit is contained in:
@@ -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;">×</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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载头像列表(懒加载,首次打开时请求)
|
||||
*/
|
||||
|
||||
@@ -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