Files
chatroom/resources/views/chat/frame.blade.php
T

257 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{--
文件功能:聊天室主界面框架(frame 页面)
全屏沉浸式布局,不使用统一 layout
CSS 抽取到 /public/css/chat.css
JS 抽取到 chat.partials.scripts Blade 模板
@author ChatRoom Laravel
@version 1.0.0
--}}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $room->name ?? '聊天室' }} - 飘落流星</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta http-equiv="Delegate-CH" content="Sec-CH-UA https://s.magsrv.com; Sec-CH-UA-Mobile https://s.magsrv.com; Sec-CH-UA-Arch https://s.magsrv.com; Sec-CH-UA-Model https://s.magsrv.com; Sec-CH-UA-Platform https://s.magsrv.com; Sec-CH-UA-Platform-Version https://s.magsrv.com; Sec-CH-UA-Bitness https://s.magsrv.com; Sec-CH-UA-Full-Version-List https://s.magsrv.com; Sec-CH-UA-Full-Version https://s.magsrv.com;">
@php
$superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100');
$myLevel = Auth::user()->user_level;
$positionPermissions = array_keys(array_filter($roomPermissionMap ?? []));
$operatorActivePosition = Auth::user()->activePosition?->load('position.department')->position;
$operatorDepartmentRank = (int) ($operatorActivePosition?->department?->rank ?? 0);
$operatorPositionRank = (int) ($operatorActivePosition?->rank ?? 0);
// 自动钓鱼状态下发给 Vite 模块,避免钓鱼面板继续在 Blade 内写业务脚本。
$autoFishingMinutesLeft = app(\App\Services\ShopService::class)->getActiveAutoFishingMinutesLeft(Auth::user());
$fishingCooldownKey = 'fishing:cd:'.Auth::id();
$fishingCooldownSeconds = \Illuminate\Support\Facades\Redis::ttl($fishingCooldownKey);
$fishingCooldownSeconds = $fishingCooldownSeconds > 0 ? $fishingCooldownSeconds : 0;
@endphp
@php
$chatbotEnabledState = \App\Models\Sysparam::getValue('chatbot_enabled', '0') === '1';
$botUserData = null;
if ($chatbotEnabledState) {
$botUser = \App\Models\User::query()->where('username', 'AI小班长')->first();
if ($botUser) {
$botUserData = app(\App\Services\ChatUserPresenceService::class)->build($botUser);
$botUserData['headfaceUrl'] = $botUser->headfaceUrl;
}
}
$activePos = Auth::user()->activePosition;
$deptName = $activePos?->position?->department?->name ?? '';
$posName = $activePos?->position?->name ?? '';
if (Auth::id() === 1) {
$myMaxReward = -1;
} else {
$pos = Auth::user()->activePosition?->position;
$myMaxReward = $pos ? ($pos->max_reward === null ? -1 : (int) $pos->max_reward) : 0;
}
$chatContext = [
'roomId' => $room->id,
'userId' => $user->id,
'username' => $user->username,
'userSex' => match ((int) $user->sex) {1 => '男', 2 => '女', default => ''},
'userLevel' => $user->user_level,
'superLevel' => $superLevel,
'sendUrl' => route('chat.send', $room->id),
'leaveUrl' => route('chat.leave', $room->id),
'expiredLeaveUrl' => \Illuminate\Support\Facades\URL::temporarySignedRoute('chat.leave.expired', now()->addHours(12), ['id' => $room->id, 'user' => $user->id]),
'heartbeatUrl' => route('chat.heartbeat', $room->id),
'fishCastUrl' => route('fishing.cast', $room->id),
'fishReelUrl' => route('fishing.reel', $room->id),
'autoFishingMinutesLeft' => (int) $autoFishingMinutesLeft,
'fishingCooldownSeconds' => (int) $fishingCooldownSeconds,
'chatBotUrl' => route('chatbot.chat'),
'chatBotClearUrl' => route('chatbot.clear'),
'chatBotEnabled' => $chatbotEnabledState,
'botUser' => $botUserData,
'hasPosition' => (bool) (Auth::user()->activePosition || Auth::user()->user_level >= $superLevel),
'hasRoomManagementPermission' => (bool) (! empty($hasRoomManagementPermission) || Auth::id() === 1),
'isSiteOwner' => Auth::id() === 1,
'positionPermissions' => $positionPermissions,
'positionPermissionMap' => $roomPermissionMap ?? [],
'welcomePrefix' => $deptName ? "{$deptName} {$posName} {$user->username}" : $user->username,
'operatorDepartmentRank' => $operatorDepartmentRank,
'operatorPositionRank' => $operatorPositionRank,
'myMaxReward' => $myMaxReward,
'appointPositionsUrl' => route('chat.appoint.positions'),
'appointUrl' => route('chat.appoint.appoint'),
'revokeUrl' => route('chat.appoint.revoke'),
'rewardUrl' => route('command.reward'),
'rewardQuotaUrl' => route('command.reward_quota'),
'refreshAllUrl' => route('command.refresh_all'),
'chatPreferencesUrl' => route('user.update_chat_preferences'),
'dailyStatusUpdateUrl' => route('user.update_daily_status'),
'dailySignInStatusUrl' => \Illuminate\Support\Facades\Route::has('daily-sign-in.status') ? route('daily-sign-in.status') : null,
'dailySignInCalendarUrl' => \Illuminate\Support\Facades\Route::has('daily-sign-in.calendar') ? route('daily-sign-in.calendar') : null,
'dailySignInClaimUrl' => \Illuminate\Support\Facades\Route::has('daily-sign-in.claim') ? route('daily-sign-in.claim') : null,
'dailySignInMakeupUrl' => \Illuminate\Support\Facades\Route::has('daily-sign-in.makeup') ? route('daily-sign-in.makeup') : null,
'userJjb' => (int) $user->jjb,
'myGold' => (int) $user->jjb,
'profileHeadface' => Auth::user()->usersf ?: '1.gif',
'profileSign' => Auth::user()->sign ?? '',
'chatPreferences' => $user->chat_preferences ?? [],
'currentDailyStatus' => $activeDailyStatus,
'dailyStatusCatalog' => $dailyStatusCatalog,
'minWeddingCost' => (int) (\App\Models\WeddingTier::query()->where('is_active', true)->orderBy('amount')->value('amount') ?? 0),
'marriage' => [
'proposeUrl' => route('marriage.propose'),
'statusUrl' => route('marriage.status'),
'targteStatusUrl' => '/marriage/target',
'myRingsUrl' => route('marriage.rings'),
'weddingTiers' => \App\Models\WeddingTier::query()->where('is_active', true)->orderBy('amount')->get(),
'defaultWeddingTierId' => \App\Models\WeddingTier::query()->where('is_active', true)->orderBy('amount')->value('id') ?? '',
'acceptUrlTemplate' => '/marriage/__ID__/accept',
'rejectUrlTemplate' => '/marriage/__ID__/reject',
'divorceUrlTemplate' => '/marriage/__ID__/divorce',
'confirmDivorceUrlTemplate' => '/marriage/__ID__/confirm-divorce',
'rejectDivorceUrlTemplate' => '/marriage/__ID__/reject-divorce',
'divorceConfigUrl' => '/marriage/divorce-config',
'weddingTiersUrl' => '/wedding/tiers',
'weddingSetupUrlTemplate' => '/wedding/__ID__/setup',
'claimEnvelopeUrlTemplate' => '/wedding/__ID__/claim',
'envelopeStatusUrlTemplate' => '/wedding/__ID__/envelope-status',
],
'earnRewardUrl' => route('earn.video_reward'),
'roomsOnlineStatusUrl' => route('chat.rooms-online-status'),
'changelogUrl' => route('changelog.index'),
'roomsIndexUrl' => route('rooms.index'),
'chatImageRetentionDays' => 3,
'initialState' => [
'historyMessages' => $historyMessages ?? [],
'localClearStorageKey' => "local_clear_msg_id_{$room->id}",
'welcomeMessage' => $initialWelcomeMessage ?? null,
'welcomeMessages' => $initialWelcomeMessages ?? [],
'entryEffect' => $newbieEffect ?: ($initialPresenceTheme['presence_effect'] ?? ($weekEffect ?? null)),
'presenceTheme' => $initialPresenceTheme ?? null,
'pendingProposal' => $pendingProposal ?? null,
'pendingDivorce' => $pendingDivorce ?? null,
],
];
@endphp
<script type="application/json" id="chat-context-data">@json($chatContext)</script>
<script>
// 兼容底部存量同步脚本,完整 URL 工厂由 Vite 的 chat-room/context.js 继续补齐。
window.chatContext = JSON.parse(document.getElementById('chat-context-data')?.textContent || '{}');
</script>
@vite(['resources/css/app.css', 'resources/css/chat.css', 'resources/js/app.js', 'resources/js/chat.js'])
</head>
<body>
<div class="chat-layout">
{{-- ═══════════ 左侧主区域 ═══════════ --}}
<div class="chat-left">
{{-- 顶部标题栏 + 公告滚动条(layout/ 子目录维护) --}}
@include('chat.partials.layout.header')
{{-- 消息窗格(双窗格,默认只显示 say1 --}}
<div class="message-panes" id="message-panes">
{{-- 主消息窗 --}}
<div class="message-pane say1" id="chat-messages-container">
<div class="msg-line">
<span style="color: #cc0000; font-weight: bold;">【公众窗口】</span>显示公众的发言!
<span class="msg-time">({{ now()->format('H:i:s') }})</span><br>
<span
style="color: #000099;">{{ $room->name }}{{ $room->description ?? '欢迎光临!畅所欲言,文明聊天。' }}</span>
</div>
</div>
{{-- 副消息窗(包厢窗) --}}
<div class="message-pane say2" id="chat-messages-container2">
<div class="msg-line">
<span style="color: #cc0000; font-weight: bold;">【包厢窗口】</span>显示包厢名单中聊友的发言!
<span class="msg-time">({{ now()->format('H:i:s') }})</span>
</div>
</div>
</div>
{{-- 底部输入工具栏(layout/ 子目录维护) --}}
@include('chat.partials.layout.input-bar')
</div>
{{-- ═══════════ 竖向工具条(layout/ 子目录维护) ═══════════ --}}
@include('chat.partials.layout.toolbar')
{{-- ═══════════ 右侧用户面板(layout/ 子目录维护) ═══════════ --}}
@include('chat.partials.layout.right-panel')
</div>
{{-- ═══════════ 手机端浮动按钮 + 抽屉(≤ 640px 屏幕有效,独立维护)═══════════ --}}
@include('chat.partials.layout.mobile-drawer')
{{-- ═══════════ 全局 UI 公共组件 ═══════════ --}}
{{-- 自定义弹窗(替代原生 alert/confirm/prompt,全页面可用) --}}
@include('chat.partials.global-dialog')
{{-- Toast 轻提示 --}}
@include('chat.partials.toast-notification')
{{-- 聊天图片大图预览层 --}}
<div id="chat-image-lightbox"
style="display:none; position:fixed; inset:0; z-index:10020; background:rgba(15,23,42,.86); backdrop-filter:blur(4px);"
data-chat-image-lightbox-close>
<div
style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; padding:32px;"
data-chat-image-lightbox-close>
<img id="chat-image-lightbox-img" src="" alt="聊天图片预览"
data-chat-image-lightbox-image
style="max-width:92vw; max-height:86vh; border-radius:12px; box-shadow:0 18px 50px rgba(0,0,0,.45);">
</div>
<button type="button" data-chat-image-lightbox-close
style="position:absolute; top:20px; right:24px; z-index:10021; border:none; background:transparent; color:#fff; font-size:34px; cursor:pointer;">&times;</button>
<div id="chat-image-lightbox-name"
style="position:absolute; left:50%; bottom:24px; transform:translateX(-50%); z-index:10021; max-width:88vw; color:#e2e8f0; font-size:12px; text-align:center; word-break:break-all;">
</div>
</div>
{{-- 大卡片通知(任命公告、好友通知、礼包选择等) --}}
@include('chat.partials.chat-banner')
{{-- ═══════════ 聊天室交互脚本(用户操作、好友通知等) ═══════════ --}}
@include('chat.partials.user-actions')
{{-- ═══════════ 活动与系统弹窗 ═══════════ --}}
{{-- 婚姻系统弹窗 --}}
@include('chat.partials.marriage-modals')
{{-- 节日福利弹窗 --}}
@include('chat.partials.holiday-modal')
@include('chat.partials.daily-sign-in-modal')
{{-- 留言板弹窗 --}}
@include('chat.partials.guestbook-modal')
{{-- 反馈弹窗 --}}
@include('chat.partials.feedback-modal')
{{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS ═══════════ --}}
{{-- deferChatGameBootstrap 已迁移到 resources/js/chat-room/game-bootstrap.js --}}
@include('chat.partials.games.baccarat-panel')
@include('chat.partials.games.slot-machine')
@include('chat.partials.games.mystery-box')
@include('chat.partials.games.horse-race-panel')
@include('chat.partials.games.fortune-panel')
@include('chat.partials.games.lottery-panel')
@include('chat.partials.games.red-packet-panel')
@include('chat.partials.games.fishing-panel')
@include('chat.partials.games.game-hall')
@include('chat.partials.games.baccarat-loss-cover-panel')
@include('chat.partials.games.gomoku-panel')
@include('chat.partials.games.earn-panel')
{{-- 全屏特效系统:管理员和会员入场可触发的全屏动效,生产环境由 Vite 压缩与版本化 --}}
@vite('resources/js/effects.js')
@include('chat.partials.scripts')
{{-- 辅助与全局事件组件 --}}
@include('chat.partials.ai-chatbot')
@include('chat.partials.system-events')
{{-- 初始历史消息、入场欢迎、进场特效、会员横幅和挂起婚姻事件已迁移到 resources/js/chat-room/initial-state.js --}}
</body>
</html>