功能:字体颜色持久化、等级体系升级至99级、钓鱼小游戏、补充系统参数
- 字体颜色:s_color 改为 varchar,发消息时保存颜色,进入聊天室自动恢复 - 等级体系:maxlevel 15→99,superlevel 16→100,99级经验阶梯(幂次曲线) - 管理权限等级按比例调整:禁言50、踢人60、设公告60、封号80、封IP90 - 钓鱼小游戏:FishingController(抛竿扣金币+收竿随机结果+广播) - 补充6个缺失的 sysparam 参数 + 4个钓鱼参数 - 用户列表点击用户名后自动聚焦输入框 - Pint 格式化
This commit is contained in:
@@ -1,3 +1,12 @@
|
||||
{{--
|
||||
文件功能:聊天室主界面框架(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">
|
||||
|
||||
@@ -5,139 +14,84 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ $room->name ?? '聊天室' }} - 飘落流星</title>
|
||||
<!-- 引入全局 CSRF Token -->
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<!-- 传递必要的上下文数据给 chat.js 模块使用 -->
|
||||
@php
|
||||
// 从 sysparam 读取权限等级配置
|
||||
$levelKick = (int) \App\Models\Sysparam::getValue('level_kick', '60');
|
||||
$levelMute = (int) \App\Models\Sysparam::getValue('level_mute', '50');
|
||||
$levelBan = (int) \App\Models\Sysparam::getValue('level_ban', '80');
|
||||
$levelBanip = (int) \App\Models\Sysparam::getValue('level_banip', '90');
|
||||
$superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100');
|
||||
@endphp
|
||||
<script>
|
||||
window.chatContext = {
|
||||
roomId: {{ $room->id }},
|
||||
username: "{{ $user->username }}",
|
||||
userLevel: {{ $user->user_level }},
|
||||
superLevel: {{ $superLevel }},
|
||||
levelKick: {{ $levelKick }},
|
||||
levelMute: {{ $levelMute }},
|
||||
levelBan: {{ $levelBan }},
|
||||
levelBanip: {{ $levelBanip }},
|
||||
sendUrl: "{{ route('chat.send', $room->id) }}",
|
||||
leaveUrl: "{{ route('chat.leave', $room->id) }}"
|
||||
leaveUrl: "{{ route('chat.leave', $room->id) }}",
|
||||
heartbeatUrl: "{{ route('chat.heartbeat', $room->id) }}",
|
||||
fishCastUrl: "{{ route('fishing.cast', $room->id) }}",
|
||||
fishReelUrl: "{{ route('fishing.reel', $room->id) }}"
|
||||
};
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js'])
|
||||
<style>
|
||||
/* 自定义滚动条样式,让界面更清爽 */
|
||||
.chat-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-scroll::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-scroll::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<link rel="stylesheet" href="/css/chat.css">
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 flex h-screen overflow-hidden text-sm">
|
||||
<body>
|
||||
<div class="chat-layout">
|
||||
|
||||
<!-- 左侧/中间部分:主聊天区域与控制台 -->
|
||||
<div class="flex-1 flex flex-col h-full bg-white relative">
|
||||
<!-- 头部房间信息栏 -->
|
||||
<header
|
||||
class="h-14 border-b bg-gradient-to-r from-blue-500 to-indigo-600 text-white flex items-center justify-between px-6 shadow-sm z-10">
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="font-bold text-lg tracking-wide">{{ $room->name }}</span>
|
||||
<span id="room-title-display"
|
||||
class="text-xs bg-white/20 px-2 py-1 rounded-full">{{ $room->description ?? '欢迎光临!' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm opacity-90"><i class="font-semibold">{{ $user->username }}</i>
|
||||
(LV.{{ $user->user_level }})</span>
|
||||
<button type="button" onclick="leaveRoom()"
|
||||
class="px-3 py-1.5 bg-white/10 hover:bg-red-500 hover:text-white rounded transition text-xs font-semibold">
|
||||
退出房间
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{{-- ═══════════ 左侧主区域 ═══════════ --}}
|
||||
<div class="chat-left">
|
||||
|
||||
<!-- 聊天记录展示区 -->
|
||||
<main id="chat-messages-container" class="flex-1 overflow-y-auto p-4 chat-scroll bg-gray-50/50 space-y-3">
|
||||
<div class="text-center text-xs text-gray-400 my-4">-- 以上是历史消息 --</div>
|
||||
<!-- 气泡动态挂载点 -->
|
||||
</main>
|
||||
{{-- 顶部标题栏 + 公告滚动条(独立文件维护) --}}
|
||||
@include('chat.partials.header')
|
||||
|
||||
<!-- 底部发言控制区 -->
|
||||
<footer class="h-auto min-h-24 bg-white border-t p-3 shadow-inner z-10">
|
||||
<form id="chat-form" onsubmit="sendMessage(event)" class="max-w-5xl mx-auto flex flex-col space-y-2">
|
||||
|
||||
<div class="flex items-center space-x-3 text-xs text-gray-600 px-1">
|
||||
<label class="flex items-center space-x-1 cursor-pointer">
|
||||
<span>对</span>
|
||||
<select id="to_user" name="to_user"
|
||||
class="border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-gray-50">
|
||||
<option value="大家" selected>所有人</option>
|
||||
<!-- 在线人员将动态加载到这里 -->
|
||||
</select>
|
||||
<span>说:</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-1 cursor-pointer">
|
||||
<span>动作:</span>
|
||||
<select id="action" name="action"
|
||||
class="border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-gray-50">
|
||||
<option value="">(无动作)</option>
|
||||
<option value="微笑">微笑</option>
|
||||
<option value="大笑">大笑</option>
|
||||
<option value="愤怒">愤怒</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-1 cursor-pointer">
|
||||
<span>字体色:</span>
|
||||
<input type="color" id="font_color" name="font_color" value="#000000"
|
||||
class="w-6 h-6 p-0 border-0 cursor-pointer rounded">
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-1 cursor-pointer hover:text-blue-600 transition">
|
||||
<input type="checkbox" id="is_secret" name="is_secret" value="1"
|
||||
class="text-blue-500 rounded focus:ring-blue-500">
|
||||
<span>悄悄话</span>
|
||||
</label>
|
||||
{{-- 消息窗格(双窗格,默认只显示 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="flex items-end space-x-2">
|
||||
<textarea id="content" name="content" rows="2"
|
||||
class="flex-1 border border-gray-300 rounded-md p-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none transition-shadow text-sm"
|
||||
placeholder="在这里输入聊天内容,按 Ctrl+Enter 快捷发送..."></textarea>
|
||||
|
||||
<button type="submit" id="send-btn"
|
||||
class="h-11 px-6 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-md shadow transition flex-shrink-0">
|
||||
发送消息
|
||||
</button>
|
||||
{{-- 副消息窗(包厢窗) --}}
|
||||
<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>
|
||||
</form>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{{-- 底部输入工具栏(独立文件维护) --}}
|
||||
@include('chat.partials.input-bar')
|
||||
</div>
|
||||
|
||||
{{-- ═══════════ 竖向工具条(独立文件维护) ═══════════ --}}
|
||||
@include('chat.partials.toolbar')
|
||||
|
||||
{{-- ═══════════ 右侧用户面板(独立文件维护) ═══════════ --}}
|
||||
@include('chat.partials.right-panel')
|
||||
</div>
|
||||
|
||||
<!-- 右侧:在线人员面板 -->
|
||||
<aside class="w-64 bg-gray-50 border-l flex flex-col h-full shadow-lg z-20">
|
||||
<div class="h-14 border-b bg-gray-100 flex items-center justify-center font-bold text-gray-700 tracking-wider">
|
||||
📶 在线人员 (<span id="online-count">0</span>)
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-2 chat-scroll">
|
||||
<ul id="online-users-list" class="space-y-1">
|
||||
<!-- 在线列表渲染点 -->
|
||||
<li class="flex items-center justify-center h-full text-xs text-gray-400 mt-10">加载中...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 弹窗容器 (Alpine 作用域外置挂载) -->
|
||||
{{-- ═══════════ 用户名片弹窗 (Alpine.js) ═══════════ --}}
|
||||
<div id="user-modal-container" x-data="{
|
||||
showUserModal: false,
|
||||
userInfo: {},
|
||||
isMuting: false,
|
||||
muteDuration: 5,
|
||||
|
||||
// 获取用户资料
|
||||
async fetchUser(username) {
|
||||
try {
|
||||
const res = await fetch('/user/' + encodeURIComponent(username));
|
||||
@@ -149,7 +103,6 @@
|
||||
}
|
||||
},
|
||||
|
||||
// 执行踢出
|
||||
async kickUser() {
|
||||
if (!confirm('确定要将 ' + this.userInfo.username + ' 踢出房间吗?')) return;
|
||||
try {
|
||||
@@ -173,7 +126,6 @@
|
||||
}
|
||||
},
|
||||
|
||||
// 执行禁言
|
||||
async muteUser() {
|
||||
try {
|
||||
const res = await fetch('/user/' + encodeURIComponent(this.userInfo.username) + '/mute', {
|
||||
@@ -200,337 +152,110 @@
|
||||
}
|
||||
}
|
||||
}">
|
||||
<!-- 用户名片弹窗 -->
|
||||
<div x-show="showUserModal" style="display: none;"
|
||||
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showUserModal = false"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
|
||||
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 h-24 relative">
|
||||
<button @click="showUserModal = false"
|
||||
class="absolute top-3 right-3 text-white/80 hover:text-white font-bold">×</button>
|
||||
<div x-show="showUserModal" style="display: none;" class="modal-overlay"
|
||||
x-on:click.self="showUserModal = false">
|
||||
<div class="modal-card" x-transition>
|
||||
{{-- 弹窗头部 --}}
|
||||
<div class="modal-header">
|
||||
<h3 x-text="'用户名片 · ' + userInfo.username"></h3>
|
||||
<button class="modal-close" x-on:click="showUserModal = false">×</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 pb-6 relative pt-12">
|
||||
<!-- 头像 (暂时用占位圆圈代替,后续从 /images/headface 读取) -->
|
||||
<div
|
||||
class="absolute -top-12 left-6 w-20 h-20 rounded-full border-4 border-white bg-gray-200 flex items-center justify-center text-gray-400 font-bold text-xl shadow-md">
|
||||
<img x-show="userInfo.headface" :src="'/images/headface/' + userInfo.headface"
|
||||
class="w-full h-full rounded-full object-cover"
|
||||
@@error="$el.style.display='none'">
|
||||
<span x-show="!userInfo.headface">Pic</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<h3 class="text-2xl font-bold text-gray-800 flex items-center space-x-2">
|
||||
<span x-text="userInfo.username"></span>
|
||||
<span
|
||||
:class="userInfo.sex === '男' ? 'bg-blue-100 text-blue-700' : (userInfo
|
||||
.sex === '女' ? 'bg-pink-100 text-pink-700' : 'bg-gray-100 text-gray-700')"
|
||||
class="text-[10px] px-2 py-0.5 rounded-full" x-text="userInfo.sex"></span>
|
||||
</h3>
|
||||
<p class="text-indigo-600 text-sm font-semibold mt-1">LV.<span
|
||||
x-text="userInfo.user_level"></span></p>
|
||||
|
||||
<div class="mt-4 bg-gray-50 border border-gray-100 rounded-lg p-3">
|
||||
<p class="text-sm text-gray-600 italic" x-text="userInfo.sign"></p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-3 border-t pt-2">加入时间: <span
|
||||
x-text="userInfo.created_at"></span></p>
|
||||
</div>
|
||||
|
||||
<!-- 特权操作区(仅超管或房主显示踢人操作)-->
|
||||
@if (Auth::user()->user_level >= 15 || $room->master == Auth::user()->username)
|
||||
<div class="mt-6 pt-4 border-t border-gray-100"
|
||||
x-show="userInfo.username !== window.chatContext.username && userInfo.user_level < {{ Auth::user()->user_level }}">
|
||||
<p class="text-xs font-bold text-red-400 mb-2">特权操作</p>
|
||||
<div class="flex space-x-2">
|
||||
<button @click="kickUser()"
|
||||
class="flex-1 bg-red-100 text-red-700 hover:bg-red-200 py-1.5 rounded-md text-sm font-bold transition">踢出房间</button>
|
||||
<button @click="isMuting = !isMuting"
|
||||
class="flex-1 bg-amber-100 text-amber-700 hover:bg-amber-200 py-1.5 rounded-md text-sm font-bold transition">禁言拦截</button>
|
||||
</div>
|
||||
|
||||
<!-- 禁言表单 -->
|
||||
<div x-show="isMuting"
|
||||
class="mt-3 bg-amber-50 rounded p-2 flex items-center space-x-2 border border-amber-200"
|
||||
style="display: none;">
|
||||
<input type="number" x-model="muteDuration"
|
||||
class="w-full border-amber-300 rounded focus:ring-amber-500 text-sm px-2 py-1"
|
||||
placeholder="分钟" min="1">
|
||||
<span class="text-xs text-amber-800 shrink-0">分钟</span>
|
||||
<button @click="muteUser()"
|
||||
class="bg-amber-500 hover:bg-amber-600 text-white px-3 py-1 rounded text-sm font-bold shrink-0 shadow-sm">执行</button>
|
||||
{{-- 弹窗内容 --}}
|
||||
<div class="modal-body">
|
||||
<div class="profile-row">
|
||||
<img class="profile-avatar" x-show="userInfo.headface"
|
||||
:src="'/images/headface/' + userInfo.headface" x-on:error="$el.style.display='none'">
|
||||
<div class="profile-info">
|
||||
<h4>
|
||||
<span x-text="userInfo.username"></span>
|
||||
<span class="level-badge" x-text="'LV.' + userInfo.user_level"></span>
|
||||
<span class="sex-badge"
|
||||
x-text="userInfo.sex === '男' ? '♂' : (userInfo.sex === '女' ? '♀' : '')"
|
||||
:style="userInfo.sex === '男' ? 'color: blue' : (userInfo.sex === '女' ?
|
||||
'color: deeppink' : '')"></span>
|
||||
</h4>
|
||||
<div style="font-size: 11px; color: #999; margin-top: 2px;">
|
||||
加入: <span x-text="userInfo.created_at"></span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="profile-detail" x-text="userInfo.sign || '这家伙很懒,什么也没留下'"></div>
|
||||
</div>
|
||||
|
||||
<!-- 常规操作:飞鸽传书 私信 -->
|
||||
<div class="px-6 pb-6 pt-2" x-show="userInfo.username !== window.chatContext.username">
|
||||
<a :href="'{{ route('guestbook.index', ['tab' => 'outbox']) }}&to=' + encodeURIComponent(userInfo
|
||||
.username)"
|
||||
target="_blank"
|
||||
class="w-full bg-pink-100 text-pink-700 hover:bg-pink-200 py-2.5 rounded-lg font-bold transition flex items-center justify-center shadow-sm text-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
|
||||
</path>
|
||||
</svg>
|
||||
飞鸽传书 (发私信)
|
||||
{{-- 操作按钮 --}}
|
||||
<div class="modal-actions" x-show="userInfo.username !== window.chatContext.username">
|
||||
<button class="btn-whisper"
|
||||
x-on:click="document.getElementById('to_user').value = userInfo.username; document.getElementById('content').focus(); showUserModal = false;">
|
||||
悄悄话
|
||||
</button>
|
||||
<a class="btn-mail"
|
||||
:href="'{{ route('guestbook.index', ['tab' => 'outbox']) }}&to=' + encodeURIComponent(userInfo
|
||||
.username)"
|
||||
target="_blank">
|
||||
写私信
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- 特权操作(管理员/房主) --}}
|
||||
@if (Auth::user()->user_level >= $levelKick || $room->master == Auth::user()->username)
|
||||
<div style="padding: 0 16px 12px;"
|
||||
x-show="userInfo.username !== window.chatContext.username && userInfo.user_level < {{ Auth::user()->user_level }}">
|
||||
<div style="font-size: 11px; color: #c00; margin-bottom: 6px; font-weight: bold;">管理操作</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="btn-kick" style="flex:1; padding: 5px; border-radius: 4px;"
|
||||
x-on:click="kickUser()">踢出</button>
|
||||
<button class="btn-mute" style="flex:1; padding: 5px; border-radius: 4px;"
|
||||
x-on:click="isMuting = !isMuting">禁言</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mute-form" x-show="isMuting" style="display: none;">
|
||||
<input type="number" x-model="muteDuration" min="1" placeholder="分钟">
|
||||
<span style="font-size: 11px; color: #b86e00;">分钟</span>
|
||||
<button x-on:click="muteUser()">执行</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 核心页面交互逻辑,连接 chat.js 抛出的自定义事件
|
||||
{{-- ═══════════ 聊天室交互脚本(独立文件维护) ═══════════ --}}
|
||||
@include('chat.partials.scripts')
|
||||
|
||||
const container = document.getElementById('chat-messages-container');
|
||||
const userList = document.getElementById('online-users-list');
|
||||
const toUserSelect = document.getElementById('to_user');
|
||||
const onlineCount = document.getElementById('online-count');
|
||||
{{-- ═══════════ 头像选择弹窗 ═══════════ --}}
|
||||
<div id="avatar-picker-modal"
|
||||
style="display:none; position:fixed; top:0; left:0; right:0; bottom:0;
|
||||
background:rgba(0,0,0,0.5); z-index:9999; justify-content:center; align-items:center;">
|
||||
<div
|
||||
style="background:#fff; width:600px; max-height:80vh; border-radius:6px; overflow:hidden;
|
||||
box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column;">
|
||||
{{-- 标题栏 --}}
|
||||
<div
|
||||
style="background:#336699; color:#fff; padding:10px 16px; font-size:14px; font-weight:bold;
|
||||
display:flex; justify-content:space-between; align-items:center;">
|
||||
<span>🖼 修改头像(原版风格)</span>
|
||||
<span style="cursor:pointer; font-size:18px;" onclick="closeAvatarPicker()">✕</span>
|
||||
</div>
|
||||
|
||||
let onlineUsers = {}; // 用于本地维护在线名单
|
||||
{{-- 预览区 --}}
|
||||
<div
|
||||
style="padding:10px 16px; background:#f0f6ff; border-bottom:1px solid #ddd; display:flex; align-items:center; gap:12px;">
|
||||
<span style="font-size:12px; color:#666;">当前选中:</span>
|
||||
<img id="avatar-preview" src="/images/headface/{{ $user->usersf ?: '1.GIF' }}"
|
||||
style="width:40px; height:40px; border:2px solid #336699; border-radius:4px;">
|
||||
<span id="avatar-selected-name" style="font-size:12px; color:#333;">{{ $user->usersf ?: '未设置' }}</span>
|
||||
<button id="avatar-save-btn" disabled onclick="saveAvatar()"
|
||||
style="margin-left:auto; padding:5px 16px; background:#336699; color:#fff; border:none;
|
||||
border-radius:3px; font-size:12px; cursor:pointer;">确定更换</button>
|
||||
</div>
|
||||
|
||||
// 辅助:滚动到底部
|
||||
function scrollToBottom() {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
{{-- 头像网格 --}}
|
||||
<div id="avatar-grid"
|
||||
style="flex:1; overflow-y:auto; padding:10px; display:flex; flex-wrap:wrap;
|
||||
gap:4px; align-content:flex-start;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// 辅助:渲染在线人员列表
|
||||
function renderUserList() {
|
||||
userList.innerHTML = '';
|
||||
// 同时更新“对谁说”下拉框(保留大家选项)
|
||||
toUserSelect.innerHTML = '<option value="大家">所有人</option>';
|
||||
|
||||
let count = 0;
|
||||
for (let username in onlineUsers) {
|
||||
count++;
|
||||
let user = onlineUsers[username];
|
||||
|
||||
// 渲染右侧面板
|
||||
let li = document.createElement('li');
|
||||
li.className =
|
||||
'px-3 py-2 hover:bg-blue-50 rounded cursor-pointer transition flex items-center justify-between border-b border-gray-100 last:border-0';
|
||||
li.innerHTML = `
|
||||
<div class="flex items-center space-x-2 truncate">
|
||||
<span class="w-2 h-2 rounded-full bg-green-500 shrink-0 shadow-[0_0_5px_rgba(34,197,94,0.5)]"></span>
|
||||
<span class="text-sm font-medium text-gray-700 truncate" title="${username}">${username}</span>
|
||||
</div>
|
||||
`;
|
||||
// 单击右侧列表可以快速查看资料 / @ 人
|
||||
li.onclick = () => {
|
||||
toUserSelect.value = username;
|
||||
// 触发 Alpine 挂载的查看名片方法
|
||||
const modalScope = document.querySelector('[x-data]').__x.$data;
|
||||
if (modalScope && username !== window.chatContext.username) {
|
||||
modalScope.fetchUser(username);
|
||||
}
|
||||
};
|
||||
userList.appendChild(li);
|
||||
|
||||
// 添加到“对谁说”列表
|
||||
if (username !== window.chatContext.username) {
|
||||
let option = document.createElement('option');
|
||||
option.value = username;
|
||||
option.textContent = username;
|
||||
toUserSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
onlineCount.innerText = count;
|
||||
}
|
||||
|
||||
// 辅助:渲染单条消息气泡
|
||||
function appendMessage(msg) {
|
||||
const isMe = msg.from_user === window.chatContext.username;
|
||||
const alignClass = isMe ? 'justify-end' : 'justify-start';
|
||||
const bubbleBg = isMe ? 'bg-blue-500 text-white' : 'bg-white border border-gray-200 text-gray-800';
|
||||
const textColorAttr = msg.font_color && msg.font_color !== '#000000' && msg.font_color !== '#000' && !isMe ?
|
||||
`color: ${msg.font_color}` : '';
|
||||
|
||||
let headerText = '';
|
||||
|
||||
// 辅助:生成可点击的用户名 HTML
|
||||
const clickableUser = (uName) =>
|
||||
`<span class="cursor-pointer hover:underline hover:text-blue-600 transition" onclick="document.querySelector('[x-data]').__x.$data.fetchUser('${uName}')">${uName}</span>`;
|
||||
|
||||
if (msg.to_user !== '大家') {
|
||||
headerText = `${clickableUser(msg.from_user)} 对 ${clickableUser(msg.to_user)} ${msg.action} 说:`;
|
||||
if (msg.is_secret) headerText = `[悄悄话] ` + headerText;
|
||||
} else {
|
||||
headerText = `${clickableUser(msg.from_user)} ${msg.action} 说:`;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = `flex ${alignClass} mb-3 group`;
|
||||
|
||||
let html = `
|
||||
<div class="max-w-[75%] flex flex-col space-y-1">
|
||||
<div class="text-[11px] text-gray-400 ${isMe ? 'text-right hidden group-hover:block transition-all' : 'text-left pl-1'}">${headerText} <span class="ml-2 font-mono">${msg.sent_at}</span></div>
|
||||
<div class="px-4 py-2 rounded-2xl shadow-sm leading-relaxed whitespace-pre-wrap word-break ${bubbleBg}" style="${textColorAttr}">${msg.content}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
div.innerHTML = html;
|
||||
container.appendChild(div);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 🚀 初始化 WebSocket 监听器
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof window.initChat === 'function') {
|
||||
window.initChat(window.chatContext.roomId);
|
||||
}
|
||||
});
|
||||
|
||||
// 🔌 监听 WebSocket 事件
|
||||
window.addEventListener('chat:here', (e) => {
|
||||
const users = e.detail;
|
||||
onlineUsers = {};
|
||||
users.forEach(u => {
|
||||
onlineUsers[u.username] = u;
|
||||
});
|
||||
renderUserList();
|
||||
});
|
||||
|
||||
window.addEventListener('chat:joining', (e) => {
|
||||
const user = e.detail;
|
||||
onlineUsers[user.username] = user;
|
||||
renderUserList();
|
||||
|
||||
// 可选:渲染一条系统提示“某某加入了房间”
|
||||
});
|
||||
|
||||
window.addEventListener('chat:leaving', (e) => {
|
||||
const user = e.detail;
|
||||
delete onlineUsers[user.username];
|
||||
renderUserList();
|
||||
});
|
||||
|
||||
window.addEventListener('chat:message', (e) => {
|
||||
const msg = e.detail;
|
||||
// 过滤私聊:如果是别人对别人的悄悄话,自己不应该显示
|
||||
if (msg.is_secret && msg.from_user !== window.chatContext.username && msg.to_user !== window.chatContext
|
||||
.username) {
|
||||
return;
|
||||
}
|
||||
appendMessage(msg);
|
||||
});
|
||||
|
||||
window.addEventListener('chat:kicked', (e) => {
|
||||
if (e.detail.username === window.chatContext.username) {
|
||||
alert("您已被管理员踢出房间!");
|
||||
window.location.href = "{{ route('home') }}";
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('chat:title-updated', (e) => {
|
||||
document.getElementById('room-title-display').innerText = e.detail.title;
|
||||
});
|
||||
|
||||
|
||||
// 📤 发送消息逻辑
|
||||
document.getElementById('content').addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
sendMessage(e);
|
||||
}
|
||||
});
|
||||
|
||||
async function sendMessage(e) {
|
||||
if (e) e.preventDefault();
|
||||
|
||||
const form = document.getElementById('chat-form');
|
||||
const formData = new FormData(form);
|
||||
const contentInput = document.getElementById('content');
|
||||
const submitBtn = document.getElementById('send-btn');
|
||||
|
||||
const content = formData.get('content').trim();
|
||||
if (!content) {
|
||||
contentInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 锁定按钮防连点
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
|
||||
try {
|
||||
const response = await fetch(window.chatContext.sendUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok && data.status === 'success') {
|
||||
// 发送成功,清空刚才的输入并获取焦点
|
||||
contentInput.value = '';
|
||||
contentInput.focus();
|
||||
} else {
|
||||
alert('发送失败: ' + (data.message || JSON.stringify(data.errors)));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('网络连接错误,消息发送失败!');
|
||||
console.error(error);
|
||||
} finally {
|
||||
// 解锁按钮
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
}
|
||||
|
||||
// 🚪 退出房间逻辑
|
||||
async function leaveRoom() {
|
||||
if (!confirm('确定要离开聊天室吗?')) return;
|
||||
|
||||
try {
|
||||
await fetch(window.chatContext.leaveUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
window.location.href = "{{ route('home') }}";
|
||||
}
|
||||
|
||||
// ⏳ 自动挂机心跳 (每 3 分钟执行一次)
|
||||
const HEARTBEAT_INTERVAL = 180 * 1000;
|
||||
setInterval(async () => {
|
||||
if (!window.chatContext || !window.chatContext.heartbeatUrl) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(window.chatContext.heartbeatUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok && data.status === 'success') {
|
||||
// 可选:在这里如果需要更新自己的名片经验条,可触发 Alpine 等级更新(如果实现了前台独立显示自己经验的功能的话)
|
||||
console.log('心跳存点成功,当前经验值:' + data.data.exp_num + ', 等级:' + data.data.user_level);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('挂机心跳断开', e);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user