功能:字体颜色持久化、等级体系升级至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:
Vendored
+18
-4
@@ -8,12 +8,26 @@ import Pusher from "pusher-js";
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
/**
|
||||
* 根据当前页面环境自动检测 WebSocket 连接参数
|
||||
*
|
||||
* - 当页面通过 HTTPS 访问时,自动使用 wss:// 协议和当前域名
|
||||
* - 当页面通过 HTTP 访问(本地开发)时,使用 .env 中的 Reverb 配置
|
||||
*/
|
||||
const isSecure = window.location.protocol === "https:";
|
||||
const wsHost =
|
||||
import.meta.env.VITE_REVERB_HOST &&
|
||||
import.meta.env.VITE_REVERB_HOST !== "127.0.0.1" &&
|
||||
import.meta.env.VITE_REVERB_HOST !== "localhost"
|
||||
? import.meta.env.VITE_REVERB_HOST
|
||||
: window.location.hostname;
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "reverb",
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https",
|
||||
wsHost: wsHost,
|
||||
wsPort: isSecure ? 443 : (import.meta.env.VITE_REVERB_PORT ?? 8080),
|
||||
wssPort: isSecure ? 443 : (import.meta.env.VITE_REVERB_PORT ?? 443),
|
||||
forceTLS: isSecure,
|
||||
enabledTransports: ["ws", "wss"],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '随机事件管理')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-gray-800">随机事件管理 (autoact)</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">管理聊天室中随机触发的好运/坏运事件,可增减经验和金币。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 新增事件表单 --}}
|
||||
<div class="p-6 border-b border-gray-100 bg-indigo-50/50">
|
||||
<h3 class="text-sm font-bold text-gray-700 mb-3">➕ 添加新事件</h3>
|
||||
<form action="{{ route('admin.autoact.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-4xl">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">事件文本
|
||||
<span class="text-gray-400 font-normal">({username} 将被替换为触发者用户名)</span></label>
|
||||
<input type="text" name="text_body" required placeholder="例:🎉 恭喜【{username}】获得 100 经验值!"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">事件类型</label>
|
||||
<select name="event_type"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border">
|
||||
<option value="good">🟢 好运(奖励)</option>
|
||||
<option value="bad">🔴 坏运(惩罚)</option>
|
||||
<option value="neutral">🟣 中性(纯文字)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">经验变化</label>
|
||||
<input type="number" name="exp_change" value="0"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border">
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">金币变化</label>
|
||||
<input type="number" name="jjb_change" value="0"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 shadow-sm transition text-sm">
|
||||
添加事件
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- 事件列表 --}}
|
||||
<div class="p-6">
|
||||
<h3 class="text-sm font-bold text-gray-700 mb-3">📋 现有事件列表(共 {{ $events->count() }} 条)</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-gray-600 text-left border-b">
|
||||
<th class="p-3 w-10">ID</th>
|
||||
<th class="p-3">事件文本</th>
|
||||
<th class="p-3 w-20">类型</th>
|
||||
<th class="p-3 w-16">经验</th>
|
||||
<th class="p-3 w-16">金币</th>
|
||||
<th class="p-3 w-16">状态</th>
|
||||
<th class="p-3 w-32">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($events as $event)
|
||||
<tr class="border-b hover:bg-gray-50 {{ !$event->enabled ? 'opacity-40' : '' }}">
|
||||
<td class="p-3 text-gray-400">{{ $event->id }}</td>
|
||||
<td class="p-3">
|
||||
<span class="text-xs">{{ Str::limit($event->text_body, 60) }}</span>
|
||||
</td>
|
||||
<td class="p-3">
|
||||
@if ($event->event_type === 'good')
|
||||
<span
|
||||
class="px-2 py-0.5 rounded-full bg-green-100 text-green-700 text-xs font-bold">好运</span>
|
||||
@elseif($event->event_type === 'bad')
|
||||
<span
|
||||
class="px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-xs font-bold">坏运</span>
|
||||
@else
|
||||
<span
|
||||
class="px-2 py-0.5 rounded-full bg-purple-100 text-purple-700 text-xs font-bold">中性</span>
|
||||
@endif
|
||||
</td>
|
||||
<td
|
||||
class="p-3 {{ $event->exp_change > 0 ? 'text-green-600' : ($event->exp_change < 0 ? 'text-red-600' : 'text-gray-400') }} font-bold">
|
||||
{{ $event->exp_change > 0 ? '+' : '' }}{{ $event->exp_change }}
|
||||
</td>
|
||||
<td
|
||||
class="p-3 {{ $event->jjb_change > 0 ? 'text-green-600' : ($event->jjb_change < 0 ? 'text-red-600' : 'text-gray-400') }} font-bold">
|
||||
{{ $event->jjb_change > 0 ? '+' : '' }}{{ $event->jjb_change }}
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<button onclick="toggleEvent({{ $event->id }}, this)"
|
||||
class="text-xs px-2 py-1 rounded {{ $event->enabled ? 'bg-green-100 text-green-700' : 'bg-gray-200 text-gray-500' }} cursor-pointer">
|
||||
{{ $event->enabled ? '启用' : '禁用' }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="p-3">
|
||||
<form action="{{ route('admin.autoact.destroy', $event->id) }}" method="POST"
|
||||
class="inline" onsubmit="return confirm('确定要删除此事件吗?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="text-red-500 hover:text-red-700 text-xs font-bold">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="p-6 text-center text-gray-400">暂无事件,请添加。</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 切换事件启用/禁用状态
|
||||
*/
|
||||
function toggleEvent(id, btn) {
|
||||
fetch(`/admin/autoact/${id}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
btn.textContent = data.enabled ? '启用' : '禁用';
|
||||
btn.className = data.enabled ?
|
||||
'text-xs px-2 py-1 rounded bg-green-100 text-green-700 cursor-pointer' :
|
||||
'text-xs px-2 py-1 rounded bg-gray-200 text-gray-500 cursor-pointer';
|
||||
// 切换行透明度
|
||||
btn.closest('tr').classList.toggle('opacity-40', !data.enabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -29,9 +29,13 @@
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.users.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
👥 用户管理
|
||||
</a>
|
||||
<a href="{{ route('admin.sql.index') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.sql.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
💾 SQL 探针
|
||||
<a href="{{ route('admin.rooms.index') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.rooms.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
🏠 房间管理
|
||||
</a>
|
||||
<a href="{{ route('admin.autoact.index') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.autoact.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
🎲 随机事件
|
||||
</a>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-white/10">
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '房间管理')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-gray-800">房间管理</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">管理聊天室房间的名称、介绍、公告和权限设置。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
@foreach ($rooms as $room)
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden" x-data="{ editing: false }">
|
||||
{{-- 房间信息行 --}}
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 cursor-pointer"
|
||||
@click="editing = !editing">
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
class="text-xs font-bold bg-indigo-100 text-indigo-700 px-2 py-1 rounded">#{{ $room->id }}</span>
|
||||
<div>
|
||||
<span class="font-bold text-gray-800">{{ $room->room_name }}</span>
|
||||
@if ($room->room_keep)
|
||||
<span
|
||||
class="ml-2 text-xs px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">系统房间</span>
|
||||
@endif
|
||||
@if (!$room->door_open)
|
||||
<span class="ml-1 text-xs px-1.5 py-0.5 rounded bg-red-100 text-red-600">已关闭</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-gray-500">
|
||||
<span>房主: {{ $room->room_owner ?: '无' }}</span>
|
||||
<span>人气: {{ $room->visit_num ?? 0 }}</span>
|
||||
<span>等级限制: ≥{{ $room->permit_level ?? 0 }}</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="editing ? 'rotate-180' : ''"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 编辑表单(展开) --}}
|
||||
<div x-show="editing" x-collapse class="border-t border-gray-200">
|
||||
<form action="{{ route('admin.rooms.update', $room->id) }}" method="POST" class="p-4">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">房间名称</label>
|
||||
<input type="text" name="room_name" value="{{ $room->room_name }}" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">房主用户名</label>
|
||||
<input type="text" name="room_owner" value="{{ $room->room_owner }}"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border text-sm"
|
||||
placeholder="留空为无房主">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">房间介绍</label>
|
||||
<input type="text" name="room_des" value="{{ $room->room_des }}"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border text-sm"
|
||||
placeholder="描述这个房间的用途和氛围">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">公告/祝福语
|
||||
<span class="text-gray-400 font-normal">(在聊天室顶部滚动显示)</span></label>
|
||||
<input type="text" name="announcement" value="{{ $room->announcement }}"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border text-sm"
|
||||
placeholder="例:祝大家新年快乐!">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">进入等级限制</label>
|
||||
<input type="number" name="permit_level" value="{{ $room->permit_level ?? 0 }}"
|
||||
min="0" max="15"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">房间状态</label>
|
||||
<select name="door_open"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 bg-white border text-sm">
|
||||
<option value="1" {{ $room->door_open ? 'selected' : '' }}>开放</option>
|
||||
<option value="0" {{ !$room->door_open ? 'selected' : '' }}>关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<button type="submit"
|
||||
class="px-5 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 shadow-sm transition text-sm">
|
||||
保存修改
|
||||
</button>
|
||||
@unless ($room->room_keep)
|
||||
<form action="{{ route('admin.rooms.destroy', $room->id) }}" method="POST"
|
||||
class="inline"
|
||||
onsubmit="return confirm('确定要删除房间「{{ $room->room_name }}」吗?此操作不可撤销!')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded-md font-bold hover:bg-red-600 transition text-sm">
|
||||
删除房间
|
||||
</button>
|
||||
</form>
|
||||
@endunless
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,98 +0,0 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'SQL 战术沙盒探针')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 shadow-sm text-sm">
|
||||
<p class="font-bold flex items-center">
|
||||
<span class="mr-2">⚠️</span> 顶级安全警告
|
||||
</p>
|
||||
<p class="mt-1 ml-6">
|
||||
此操作直接连通底层 MySQL 数据库。为杜绝《删库跑路》等生产事故,本控制台已硬编码拦截过滤:只会放行以 <code>SELECT</code>, <code>SHOW</code>,
|
||||
<code>EXPLAIN</code> 等起手的<strong>纯只读语句</strong>。所有的增删改一律阻断。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||||
<div class="p-6">
|
||||
<form action="{{ route('admin.sql.execute') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">输入原始只读 SQL 语句</label>
|
||||
<textarea name="query" rows="5" required placeholder="SELECT * FROM users ORDER BY id DESC LIMIT 10;"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-4 bg-gray-50 border font-mono resize-y">{{ old('query', $query ?? '') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-slate-800 text-white rounded-md font-bold hover:bg-slate-900 shadow-sm transition flex items-center">
|
||||
<span>🔥 探 针 发 射</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 结果展示区 --}}
|
||||
@isset($error)
|
||||
<div
|
||||
class="bg-red-50 border border-red-200 text-red-700 p-6 rounded-xl shadow-sm mb-6 overflow-x-auto font-mono text-sm whitespace-pre-wrap">
|
||||
{{ $error }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@isset($results)
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="bg-gray-50 px-6 py-3 border-b flex justify-between items-center text-sm font-bold text-gray-700">
|
||||
<span>查询结果 (共 {{ count($results) }} 条)</span>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto p-4 max-h-[600px] custom-scrollbar overflow-y-auto">
|
||||
@if (empty($results))
|
||||
<div class="text-center text-gray-400 py-10 font-bold">SQL 执行成功,但返回了空结果集 (0 rows)</div>
|
||||
@else
|
||||
<table class="w-full text-left border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="border-b-2 border-indigo-500">
|
||||
@foreach ($columns as $col)
|
||||
<th
|
||||
class="p-3 font-bold text-gray-600 whitespace-nowrap bg-indigo-50/50 sticky top-0 z-10 shadow-sm">
|
||||
{{ $col }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 font-mono">
|
||||
@foreach ($results as $row)
|
||||
<tr class="hover:bg-amber-50 transition">
|
||||
@foreach ($columns as $col)
|
||||
<td class="p-3 whitespace-nowrap text-gray-700">{{ $row->$col ?? 'NULL' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
<style>
|
||||
/* 针对该表格页加深一点滚动条以便查看超长字段 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #94a3b8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@endsection
|
||||
@@ -4,7 +4,11 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6" x-data="{ showEditModal: false, editingUser: {} }">
|
||||
@php
|
||||
// 管理员级别 = 最高等级 + 1,后台编辑最高可设到管理员级别
|
||||
$adminLevel = (int) \App\Models\Sysparam::getValue('maxlevel', '15') + 1;
|
||||
@endphp
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6" x-data="{ showEditModal: false, editingUser: {}, adminLevel: {{ $adminLevel }} }">
|
||||
<div class="p-6 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
|
||||
<form action="{{ route('admin.users.index') }}" method="GET" class="flex gap-2">
|
||||
<input type="text" name="username" value="{{ request('username') }}" placeholder="搜索用户名..."
|
||||
@@ -26,7 +30,7 @@
|
||||
<th class="p-4">注册名</th>
|
||||
<th class="p-4">性别</th>
|
||||
<th class="p-4">等级</th>
|
||||
<th class="p-4">个性签名</th>
|
||||
<th class="p-4">经验</th>
|
||||
<th class="p-4">注册时间</th>
|
||||
<th class="p-4 text-right">管理操作</th>
|
||||
</tr>
|
||||
@@ -42,20 +46,32 @@
|
||||
<span class="font-bold text-gray-800">{{ $user->username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-4 text-sm">{{ $user->sex }}</td>
|
||||
<td class="p-4 text-sm">{{ [0 => '保密', 1 => '男', 2 => '女'][$user->sex] ?? '保密' }}</td>
|
||||
<td class="p-4">
|
||||
<span
|
||||
class="px-2 py-0.5 rounded-full text-xs {{ $user->user_level >= 15 ? 'bg-red-100 text-red-700 font-bold' : 'bg-gray-100 text-gray-600' }}">
|
||||
class="px-2 py-0.5 rounded-full text-xs {{ $user->user_level >= 100 ? 'bg-red-100 text-red-700 font-bold' : 'bg-gray-100 text-gray-600' }}">
|
||||
LV.{{ $user->user_level }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-4 text-sm text-gray-500 truncate max-w-[200px]" title="{{ $user->sign }}">
|
||||
{{ $user->sign ?: '-' }}</td>
|
||||
<td class="p-4 text-sm font-mono text-gray-600">
|
||||
{{ number_format($user->exp_num ?? 0) }}
|
||||
</td>
|
||||
<td class="p-4 text-sm font-mono text-gray-500">{{ $user->created_at->format('Y/m/d H:i') }}
|
||||
</td>
|
||||
<td class="p-4 text-right space-x-2 relative" x-data>
|
||||
<button
|
||||
@click="editingUser = { id: {{ $user->id }}, username: '{{ addslashes($user->username) }}', user_level: {{ $user->user_level }}, sex: '{{ $user->sex }}', requestUrl: '{{ route('admin.users.update', $user->id) }}' }; showEditModal = true"
|
||||
@click="editingUser = {
|
||||
id: {{ $user->id }},
|
||||
username: '{{ addslashes($user->username) }}',
|
||||
user_level: {{ $user->user_level }},
|
||||
exp_num: {{ $user->exp_num ?? 0 }},
|
||||
jjb: {{ $user->jjb ?? 0 }},
|
||||
meili: {{ $user->meili ?? 0 }},
|
||||
sex: '{{ $user->sex }}',
|
||||
qianming: '{{ addslashes($user->qianming ?? '') }}',
|
||||
visit_num: {{ $user->visit_num ?? 0 }},
|
||||
requestUrl: '{{ route('admin.users.update', $user->id) }}'
|
||||
}; showEditModal = true"
|
||||
class="text-xs bg-indigo-50 text-indigo-600 font-bold px-3 py-1.5 rounded hover:bg-indigo-600 hover:text-white transition cursor-pointer">
|
||||
详细 / 修改
|
||||
</button>
|
||||
@@ -86,48 +102,86 @@
|
||||
<div x-show="showEditModal" style="display: none;"
|
||||
class="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4">
|
||||
<div @click.away="showEditModal = false"
|
||||
class="bg-white rounded-xl shadow-2xl w-full max-w-md transform transition-all" x-transition>
|
||||
class="bg-white rounded-xl shadow-2xl w-full max-w-lg transform transition-all" x-transition>
|
||||
<div
|
||||
class="bg-indigo-900 border-b border-indigo-800 px-6 py-4 flex justify-between items-center rounded-t-xl text-white">
|
||||
<h3 class="font-bold text-lg">全量修改:<span x-text="editingUser.username" class="text-indigo-300"></span>
|
||||
<h3 class="font-bold text-lg">编辑用户:<span x-text="editingUser.username" class="text-indigo-300"></span>
|
||||
</h3>
|
||||
<button @click="showEditModal = false" class="text-gray-400 hover:text-white">×</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- 依靠 Alpine 绑定的 AJAX 或者 Form 提交 -->
|
||||
<form :action="editingUser.requestUrl" method="POST" id="adminUserUpdateForm">
|
||||
<form :action="editingUser.requestUrl" method="POST">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">安全等级 (0-99)</label>
|
||||
<input type="number" name="user_level" x-model="editingUser.user_level" required
|
||||
class="w-full border-gray-300 rounded shadow-sm focus:ring-indigo-500">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{-- 等级 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">等级
|
||||
<span class="text-gray-400 font-normal">(最高 <span x-text="adminLevel"></span>
|
||||
级 / 管理员级别)</span></label>
|
||||
<input type="number" name="user_level" x-model="editingUser.user_level" required
|
||||
min="0" :max="adminLevel"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
</div>
|
||||
{{-- 经验 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">经验值</label>
|
||||
<input type="number" name="exp_num" x-model="editingUser.exp_num" required min="0"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
</div>
|
||||
{{-- 金币 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">金币 (jjb)</label>
|
||||
<input type="number" name="jjb" x-model="editingUser.jjb" required min="0"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
</div>
|
||||
{{-- 魅力 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">魅力值</label>
|
||||
<input type="number" name="meili" x-model="editingUser.meili" required min="0"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
</div>
|
||||
{{-- 性别 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">性别</label>
|
||||
<select name="sex" x-model="editingUser.sex"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
<option value="1">男</option>
|
||||
<option value="2">女</option>
|
||||
<option value="0">保密</option>
|
||||
</select>
|
||||
</div>
|
||||
{{-- 访问次数 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">访问次数</label>
|
||||
<input type="text" disabled :value="editingUser.visit_num"
|
||||
class="w-full bg-gray-100 border-gray-200 rounded-md p-2 border text-sm text-gray-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">性别</label>
|
||||
<select name="sex" x-model="editingUser.sex"
|
||||
class="w-full border-gray-300 rounded shadow-sm focus:ring-indigo-500">
|
||||
<option value="男">男</option>
|
||||
<option value="女">女</option>
|
||||
<option value="保密">保密</option>
|
||||
</select>
|
||||
{{-- 签名 --}}
|
||||
<div class="mt-4">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">个性签名</label>
|
||||
<input type="text" name="qianming" x-model="editingUser.qianming" maxlength="255"
|
||||
placeholder="暂无签名"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
{{-- 密码 --}}
|
||||
<div class="mt-4">
|
||||
<label
|
||||
class="block text-sm font-bold pl-1 text-red-600 border-l-4 border-red-500 bg-red-50 p-2 mb-2">强制重算密码
|
||||
(留空则不修改)</label>
|
||||
<input type="text" name="password" placeholder="强行输入新密码覆盖"
|
||||
class="w-full border-red-300 rounded shadow-sm focus:border-red-500 focus:ring-red-500 placeholder-red-300">
|
||||
class="block text-xs font-bold pl-2 text-red-600 border-l-4 border-red-500 bg-red-50 p-2 mb-1">强制重置密码
|
||||
<span class="font-normal text-gray-500">(留空不修改)</span></label>
|
||||
<input type="text" name="password" placeholder="输入新密码"
|
||||
class="w-full border-red-300 rounded-md shadow-sm focus:border-red-500 focus:ring-red-500 p-2 border text-sm placeholder-red-300">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-100">
|
||||
<div class="flex justify-end space-x-3 pt-4 mt-4 border-t border-gray-100">
|
||||
<button type="button" @click="showEditModal = false"
|
||||
class="px-4 py-2 border rounded font-medium text-gray-600 hover:bg-gray-50">取消</button>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded font-bold hover:bg-indigo-700 shadow-sm">提交强制改写</button>
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded font-bold hover:bg-indigo-700 shadow-sm">保存修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{{--
|
||||
文件功能:聊天室顶部标题栏 + 公告滚动条
|
||||
从 frame.blade.php 拆分,便于独立维护
|
||||
|
||||
依赖变量:$room
|
||||
--}}
|
||||
|
||||
{{-- 顶部标题栏(第一行:房间名称 + 介绍) --}}
|
||||
<div class="room-title-bar">
|
||||
<div class="room-name">●{{ $room->name }}●<span style="font-size:12px; font-weight:normal;">在线<span
|
||||
id="online-count">0</span>人</span></div>
|
||||
<div class="room-desc" id="room-title-display">{{ $room->description ?? '欢迎光临本聊天室!畅所欲言,文明聊天。' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 第二行:公告/祝福语滚动条(有权限的用户可设置) --}}
|
||||
<div class="room-announcement-bar" id="announcement-bar">
|
||||
<marquee behavior="scroll" direction="left" scrollamount="3" style="color:#cc0000; font-size:12px; font-weight:bold;"
|
||||
id="announcement-text">
|
||||
{{ $room->announcement ?? '欢迎来到聊天室!祝大家聊天愉快!' }}
|
||||
</marquee>
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
{{--
|
||||
文件功能:聊天室底部输入工具栏(两行结构)
|
||||
第一行:发送对象、动作、字色、悄悄话、滚屏、分屏、管理操作
|
||||
第二行:输入框 + 发送按钮
|
||||
从 frame.blade.php 拆分,便于独立维护
|
||||
|
||||
依赖变量:$user, $room, $levelKick, $levelMute, $levelBan, $levelBanip
|
||||
--}}
|
||||
|
||||
<div class="input-bar">
|
||||
<form id="chat-form" onsubmit="sendMessage(event)">
|
||||
{{-- 第一行:工具选项 --}}
|
||||
<div class="input-row">
|
||||
<label>对
|
||||
<select id="to_user" name="to_user" style="color: #224466;">
|
||||
<option value="大家" selected>大家</option>
|
||||
</select>
|
||||
说:
|
||||
</label>
|
||||
|
||||
<label>动作:
|
||||
<select id="action" name="action">
|
||||
<option value="">无</option>
|
||||
<option value="微笑">微笑</option>
|
||||
<option value="大笑">大笑</option>
|
||||
<option value="愤怒">愤怒</option>
|
||||
<option value="哭泣">哭泣</option>
|
||||
<option value="害羞">害羞</option>
|
||||
<option value="鄙视">鄙视</option>
|
||||
<option value="得意">得意</option>
|
||||
<option value="疑惑">疑惑</option>
|
||||
<option value="同情">同情</option>
|
||||
<option value="无奈">无奈</option>
|
||||
<option value="拳打">拳打</option>
|
||||
<option value="飞吻">飞吻</option>
|
||||
<option value="偷看">偷看</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>字色:
|
||||
<input type="color" id="font_color" name="font_color" value="{{ $user->s_color ?? '#000000' }}"
|
||||
style="width: 22px; height: 18px; padding: 0; border: 1px solid navy; cursor: pointer;">
|
||||
</label>
|
||||
|
||||
<label title="仅对方和自己可见">
|
||||
<input type="checkbox" id="is_secret" name="is_secret" value="1">
|
||||
悄悄话
|
||||
</label>
|
||||
|
||||
<label title="自动滚屏到最新消息">
|
||||
<input type="checkbox" id="auto_scroll" checked>
|
||||
滚屏
|
||||
</label>
|
||||
|
||||
<label>分屏:
|
||||
<select id="split_screen" onchange="changeSplitScreen(this.value)">
|
||||
<option value="0">单窗</option>
|
||||
<option value="1">上下分</option>
|
||||
</select>
|
||||
</label>
|
||||
{{-- 管理操作(根据权限显示) --}}
|
||||
@if ($user->user_level >= $levelKick || $room->master == $user->username)
|
||||
<select id="admin-action-select"
|
||||
style="font-size: 11px; padding: 1px 2px; border: 1px solid #8ab; border-radius: 2px; color: #c00;">
|
||||
<option value="">管理操作 ▾</option>
|
||||
@if ($user->user_level >= $levelKick || $room->master == $user->username)
|
||||
<option value="kick">踢出房间</option>
|
||||
@endif
|
||||
@if ($user->user_level >= $levelMute || $room->master == $user->username)
|
||||
<option value="mute">禁言</option>
|
||||
@endif
|
||||
@if ($user->user_level >= $levelBan)
|
||||
<option value="ban">封号</option>
|
||||
@endif
|
||||
@if ($user->user_level >= $levelBanip)
|
||||
<option value="banip">封IP</option>
|
||||
@endif
|
||||
</select>
|
||||
<button type="button" onclick="executeAdminAction()"
|
||||
style="font-size: 11px; padding: 1px 6px; background: #c44; color: #fff; border: none; border-radius: 2px; cursor: pointer;">执行</button>
|
||||
@endif
|
||||
|
||||
@if (
|
||||
$user->user_level >= (int) \App\Models\Sysparam::getValue('level_announcement', '10') ||
|
||||
$room->master == $user->username)
|
||||
<button type="button" onclick="promptAnnouncement()"
|
||||
style="font-size: 11px; padding: 1px 6px; background: #4a9; color: #fff; border: none; border-radius: 2px; cursor: pointer;">设公告</button>
|
||||
@endif
|
||||
|
||||
<button type="button" id="fishing-btn" onclick="startFishing()"
|
||||
style="font-size: 11px; padding: 1px 6px; background: #2563eb; color: #fff; border: none; border-radius: 2px; cursor: pointer;">🎣
|
||||
钓鱼</button>
|
||||
</div>
|
||||
|
||||
{{-- 第二行:输入框 + 发送 --}}
|
||||
<div class="input-row">
|
||||
<input type="text" id="content" name="content" class="say-input" placeholder="在这里输入聊天内容,按 Enter 发送..."
|
||||
autocomplete="off">
|
||||
<button type="submit" id="send-btn" class="send-btn">发送</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,91 @@
|
||||
{{--
|
||||
文件功能:右侧用户面板(复刻原版)
|
||||
包含:名单/房间/贴图/酷库 四个 Tab 面板
|
||||
独立文件方便维护
|
||||
|
||||
依赖变量:$room(当前房间模型)
|
||||
--}}
|
||||
<div class="chat-right">
|
||||
{{-- Tab 标题栏(原版:名单/房间/贴图/酷库) --}}
|
||||
<div class="right-tabs">
|
||||
<button class="tab-btn active" id="tab-users" onclick="switchTab('users')">名单</button>
|
||||
<button class="tab-btn" id="tab-rooms" onclick="switchTab('rooms')">房间</button>
|
||||
<button class="tab-btn" id="tab-emoji" onclick="switchTab('emoji')">贴图</button>
|
||||
<button class="tab-btn" id="tab-action" onclick="switchTab('action')">酷库</button>
|
||||
</div>
|
||||
|
||||
{{-- 用户列表面板 --}}
|
||||
<div class="user-list-content" id="panel-users">
|
||||
{{-- 房间信息头部(原版风格:房间名 + 人气 + 在线人数) --}}
|
||||
<div style="text-align:center; padding:4px 6px; border-bottom:1px solid #cde; background:#f0f6ff;">
|
||||
<div style="color:#336699; font-weight:bold; font-size:12px;">【{{ $room->name }}】</div>
|
||||
<div style="color:#cc6600; font-size:11px;">人气:{{ $room->visit_num ?? 0 }}</div>
|
||||
<div style="font-size:11px; color:#999; margin-top:2px;">
|
||||
<span id="online-count-bottom">0</span>人
|
||||
<a href="#" onclick="renderUserList(); return false;"
|
||||
style="color:#c00; font-size:10px; margin-left:4px;">刷新</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 排序 + 搜索(原版风格) --}}
|
||||
<div style="padding:3px 4px; border-bottom:1px solid #cde; background:#f8fbff; font-size:11px;">
|
||||
<div style="display:flex; align-items:center; gap:4px; margin-bottom:3px;">
|
||||
<span style="color:#666; flex-shrink:0;">排序:</span>
|
||||
<select id="user-sort-select" onchange="renderUserList()"
|
||||
style="flex:1; font-size:11px; padding:1px; border:1px solid #aac; border-radius:2px;">
|
||||
<option value="default">默认</option>
|
||||
<option value="name">按名称</option>
|
||||
<option value="level">按等级</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:2px;">
|
||||
<input type="text" id="user-search-input" placeholder="搜索用户" onkeyup="filterUserList()"
|
||||
style="width:100%; font-size:11px; padding:2px 4px; border:1px solid #aac; border-radius:2px; box-sizing:border-box;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 用户列表 --}}
|
||||
<div id="online-users-list" style="padding:2px;">
|
||||
<div style="text-align:center; color:#999; padding:20px 0; font-size:11px;">加载中...</div>
|
||||
</div>
|
||||
{{-- 管理员名单链接 --}}
|
||||
<div style="text-align:center; padding:4px; border-top:1px solid #cde; font-size:11px;">
|
||||
<a href="#" style="color:#336699; font-weight:bold;">管理员名单</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 房间列表面板 --}}
|
||||
<div class="user-list-content" id="panel-rooms" style="display: none;">
|
||||
<div style="text-align: center; color: #999; padding: 20px 0; font-size: 11px;">
|
||||
<a href="{{ route('rooms.index') }}" target="_blank">打开大厅查看房间列表</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 贴图面板 --}}
|
||||
<div class="user-list-content" id="panel-emoji" style="display: none; padding: 6px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2px; justify-content: center;">
|
||||
@for ($i = 1; $i <= 50; $i++)
|
||||
<img src="/images/emoji/{{ $i }}.gif" width="24" height="24"
|
||||
style="cursor: pointer; border: 1px solid transparent; border-radius: 2px;"
|
||||
onmouseover="this.style.borderColor='#336699'" onmouseout="this.style.borderColor='transparent'"
|
||||
onclick="insertEmoji('[IMG]{{ $i }}[/IMG]')" title="表情{{ $i }}"
|
||||
onerror="this.style.display='none'">
|
||||
@endfor
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 酷库面板 --}}
|
||||
<div class="user-list-content" id="panel-action" style="display: none; padding: 6px;">
|
||||
<div style="font-size: 11px; color: #666; line-height: 2;">
|
||||
@foreach (['微笑', '大笑', '愤怒', '哭泣', '害羞', '鄙视', '得意', '疑惑', '同情', '无奈', '拳打', '飞吻', '偷看', '战战兢兢'] as $act)
|
||||
<a href="#" onclick="setAction('{{ $act }}'); return false;"
|
||||
style="display: inline-block; padding: 2px 6px; background: #f0f6ff; border: 1px solid #dde8ff; border-radius: 3px; margin: 1px; color: #336699; font-size: 11px;">{{ $act }}</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 底部在线统计 --}}
|
||||
<div class="online-stats">
|
||||
在线: <strong id="online-count-footer">0</strong> 人
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,934 @@
|
||||
{{--
|
||||
文件功能:聊天室前端交互脚本(Blade 模板形式)
|
||||
包含消息渲染、用户列表管理、发送消息、WebSocket 事件监听、
|
||||
管理操作、存点心跳、头像选择等全部前端逻辑
|
||||
|
||||
通过 @include('chat.partials.scripts') 引入到 frame.blade.php
|
||||
|
||||
@author ChatRoom Laravel
|
||||
@version 1.0.0
|
||||
--}}
|
||||
<script>
|
||||
/**
|
||||
* 聊天室前端交互逻辑
|
||||
* 保留所有 WebSocket 事件监听,复刻原版 UI 交互
|
||||
*/
|
||||
|
||||
// ── DOM 元素引用 ──────────────────────────────────────
|
||||
const container = document.getElementById('chat-messages-container');
|
||||
const container2 = document.getElementById('chat-messages-container2');
|
||||
const userList = document.getElementById('online-users-list');
|
||||
const toUserSelect = document.getElementById('to_user');
|
||||
const onlineCount = document.getElementById('online-count');
|
||||
const onlineCountBottom = document.getElementById('online-count-bottom');
|
||||
|
||||
let onlineUsers = {};
|
||||
let autoScroll = true;
|
||||
|
||||
// ── Tab 切换 ──────────────────────────────────────
|
||||
function switchTab(tab) {
|
||||
['users', 'rooms', 'emoji', 'action'].forEach(t => {
|
||||
document.getElementById('panel-' + t).style.display = t === tab ? 'block' : 'none';
|
||||
document.getElementById('tab-' + t).classList.toggle('active', t === tab);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 分屏切换 ──────────────────────────────────────
|
||||
function changeSplitScreen(mode) {
|
||||
const panes = document.getElementById('message-panes');
|
||||
if (mode === '1') {
|
||||
panes.classList.add('split-h');
|
||||
} else {
|
||||
panes.classList.remove('split-h');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 表情插入 ──────────────────────────────────────
|
||||
function insertEmoji(code) {
|
||||
const input = document.getElementById('content');
|
||||
input.value += code;
|
||||
input.focus();
|
||||
switchTab('users');
|
||||
}
|
||||
|
||||
// ── 动作选择 ──────────────────────────────────────
|
||||
function setAction(act) {
|
||||
document.getElementById('action').value = act;
|
||||
switchTab('users');
|
||||
document.getElementById('content').focus();
|
||||
}
|
||||
|
||||
// ── 自动滚屏 ──────────────────────────────────────
|
||||
document.getElementById('auto_scroll').addEventListener('change', function() {
|
||||
autoScroll = this.checked;
|
||||
});
|
||||
|
||||
// ── 滚动到底部 ───────────────────────────────────
|
||||
function scrollToBottom() {
|
||||
if (autoScroll) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 渲染在线人员列表(支持排序) ──────────────────
|
||||
function renderUserList() {
|
||||
userList.innerHTML = '';
|
||||
toUserSelect.innerHTML = '<option value="大家">大家</option>';
|
||||
|
||||
// 在列表顶部添加"大家"条目(原版风格)
|
||||
let allDiv = document.createElement('div');
|
||||
allDiv.className = 'user-item';
|
||||
allDiv.innerHTML = '<span class="user-name" style="padding-left: 4px; color: navy;">大家</span>';
|
||||
allDiv.onclick = () => {
|
||||
toUserSelect.value = '大家';
|
||||
};
|
||||
userList.appendChild(allDiv);
|
||||
|
||||
// 获取排序方式
|
||||
const sortSelect = document.getElementById('user-sort-select');
|
||||
const sortBy = sortSelect ? sortSelect.value : 'default';
|
||||
|
||||
// 构建用户数组并排序
|
||||
let userArr = [];
|
||||
for (let username in onlineUsers) {
|
||||
userArr.push({
|
||||
username,
|
||||
...onlineUsers[username]
|
||||
});
|
||||
}
|
||||
|
||||
if (sortBy === 'name') {
|
||||
userArr.sort((a, b) => a.username.localeCompare(b.username, 'zh'));
|
||||
} else if (sortBy === 'level') {
|
||||
userArr.sort((a, b) => (b.user_level || 0) - (a.user_level || 0));
|
||||
}
|
||||
|
||||
let count = userArr.length;
|
||||
|
||||
userArr.forEach(user => {
|
||||
const username = user.username;
|
||||
let item = document.createElement('div');
|
||||
item.className = 'user-item';
|
||||
item.dataset.username = username;
|
||||
|
||||
const headface = user.headface || '1.GIF';
|
||||
|
||||
item.innerHTML = `
|
||||
<img class="user-head" src="/images/headface/${headface}" onerror="this.src='/images/headface/1.GIF'">
|
||||
<span class="user-name">${username}</span>
|
||||
`;
|
||||
|
||||
item.onclick = () => {
|
||||
toUserSelect.value = username;
|
||||
document.getElementById('content').focus();
|
||||
};
|
||||
item.ondblclick = () => {
|
||||
if (username !== window.chatContext.username) {
|
||||
showUserInfoInSay2(username);
|
||||
}
|
||||
};
|
||||
userList.appendChild(item);
|
||||
|
||||
if (username !== window.chatContext.username) {
|
||||
let option = document.createElement('option');
|
||||
option.value = username;
|
||||
option.textContent = username;
|
||||
toUserSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
|
||||
onlineCount.innerText = count;
|
||||
onlineCountBottom.innerText = count;
|
||||
const footer = document.getElementById('online-count-footer');
|
||||
if (footer) footer.innerText = count;
|
||||
|
||||
// 如果有搜索关键词,重新过滤
|
||||
filterUserList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索/过滤用户列表
|
||||
*/
|
||||
function filterUserList() {
|
||||
const searchInput = document.getElementById('user-search-input');
|
||||
const keyword = searchInput ? searchInput.value.trim().toLowerCase() : '';
|
||||
const items = userList.querySelectorAll('.user-item');
|
||||
|
||||
items.forEach(item => {
|
||||
if (!keyword) {
|
||||
item.style.display = '';
|
||||
return;
|
||||
}
|
||||
const name = (item.dataset.username || item.textContent || '').toLowerCase();
|
||||
item.style.display = name.includes(keyword) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// ── 点击消息中的用户名,切换发言对象 ──────────
|
||||
function switchTarget(username) {
|
||||
const options = toUserSelect.options;
|
||||
let found = false;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (options[i].value === username) {
|
||||
toUserSelect.value = username;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果不在列表中(可能已离线),临时添加
|
||||
if (!found && username !== '大家') {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = username;
|
||||
opt.textContent = username;
|
||||
toUserSelect.appendChild(opt);
|
||||
toUserSelect.value = username;
|
||||
}
|
||||
// 切换目标后自动聚焦输入框,方便直接输入
|
||||
document.getElementById('content').focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 双击用户名 → 在包厢窗口(say2)显示用户基本信息
|
||||
*/
|
||||
async function showUserInfoInSay2(username) {
|
||||
try {
|
||||
const res = await fetch('/user/' + encodeURIComponent(username));
|
||||
const info = await res.json();
|
||||
|
||||
const sexText = info.sex === '女' ? '女' : '男';
|
||||
const level = info.user_level || 0;
|
||||
const exp = info.exp_num || 0;
|
||||
const jjb = info.jjb || 0;
|
||||
const sign = info.qianming || info.sign || '暂无';
|
||||
|
||||
const lines = [
|
||||
`══════ <b style="color:#336699;">${info.username || username}</b> 的资料 ══════`,
|
||||
`性别:${sexText} 等级:${level} 经验:${exp} 金币:${jjb}`,
|
||||
`签名:${sign}`,
|
||||
`════════════════════════`
|
||||
];
|
||||
|
||||
lines.forEach(text => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg-line';
|
||||
div.innerHTML = `<span style="color:#666; font-size:12px;">${text}</span>`;
|
||||
container2.appendChild(div);
|
||||
});
|
||||
container2.scrollTop = container2.scrollHeight;
|
||||
} catch (e) {
|
||||
console.error('获取用户资料失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加消息到聊天窗格(原版风格:非气泡模式,逐行显示)
|
||||
*/
|
||||
function appendMessage(msg) {
|
||||
const isMe = msg.from_user === window.chatContext.username;
|
||||
const fontColor = msg.font_color || '#000000';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg-line';
|
||||
|
||||
const timeStr = msg.sent_at || '';
|
||||
|
||||
// 用户名(单击切换发言对象,双击查看资料)
|
||||
const clickableUser = (uName, color) =>
|
||||
`<span class="msg-user" style="color: ${color}; cursor: pointer;" onclick="switchTarget('${uName}')" ondblclick="showUserInfoInSay2('${uName}')">${uName}</span>`;
|
||||
|
||||
// 获取发言者头像
|
||||
const senderInfo = onlineUsers[msg.from_user];
|
||||
const senderHead = (senderInfo && senderInfo.headface) || '1.GIF';
|
||||
const headImg =
|
||||
`<img src="/images/headface/${senderHead}" style="display:inline;width:16px;height:16px;vertical-align:middle;margin-right:2px;" onerror="this.src='/images/headface/1.GIF'">`;
|
||||
|
||||
let html = '';
|
||||
|
||||
if (msg.is_secret) {
|
||||
// 悄悄话样式(原版:紫色斜体)
|
||||
html =
|
||||
`<span class="msg-secret">${headImg}${clickableUser(msg.from_user, '#cc00cc')}对${clickableUser(msg.to_user, '#cc00cc')}`;
|
||||
if (msg.action) html += `${msg.action}`;
|
||||
html += `悄悄说:${msg.content}</span>`;
|
||||
} else if (msg.to_user && msg.to_user !== '大家') {
|
||||
html = `${headImg}${clickableUser(msg.from_user, '#000099')}对${clickableUser(msg.to_user, '#000099')}`;
|
||||
if (msg.action) html += `${msg.action}`;
|
||||
html += `说:<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
|
||||
} else {
|
||||
html = `${headImg}${clickableUser(msg.from_user, '#000099')}对大家`;
|
||||
if (msg.action) html += `${msg.action}`;
|
||||
html += `说:<span class="msg-content" style="color: ${fontColor}">${msg.content}</span>`;
|
||||
}
|
||||
|
||||
html += ` <span class="msg-time">(${timeStr})</span>`;
|
||||
div.innerHTML = html;
|
||||
|
||||
// 路由规则(复刻原版):
|
||||
// 公众窗口(say1):别人的公聊消息
|
||||
// 包厢窗口(say2):自己发的消息 + 悄悄话 + 对自己说的消息
|
||||
const isRelatedToMe = isMe ||
|
||||
msg.is_secret ||
|
||||
msg.to_user === window.chatContext.username;
|
||||
|
||||
if (isRelatedToMe) {
|
||||
container2.appendChild(div);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
} else {
|
||||
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();
|
||||
|
||||
// 原版风格:完整句式的随机趣味欢迎语
|
||||
const gender = user.sex === '女' ? '美女' : '帅哥';
|
||||
const uname = user.username;
|
||||
const welcomeTemplates = [
|
||||
`${gender}<b>${uname}</b>开着刚买不久的车,来到了,见到各位大虾,拱手曰:"众位大虾,小生有礼了"`,
|
||||
`${gender}<b>${uname}</b>骑着小毛驴哼着小调,悠闲地走了进来,对大家嘿嘿一笑`,
|
||||
`${gender}<b>${uname}</b>坐着豪华轿车缓缓驶入,推门而出,拍了拍身上的灰,霸气说道:"我来也!"`,
|
||||
`${gender}<b>${uname}</b>踩着七彩祥云从天而降,众人皆惊,抱拳道:"各位久等了!"`,
|
||||
`${gender}<b>${uname}</b>划着小船飘然而至,微微一笑,翩然上岸`,
|
||||
`${gender}<b>${uname}</b>骑着自行车铃铛叮当响,远远就喊:"我来啦!想我没?"`,
|
||||
`${gender}<b>${uname}</b>开着拖拉机突突突地开了进来,下车后拍了拍手说:"交通不便,来迟了!"`,
|
||||
`${gender}<b>${uname}</b>坐着火箭嗖的一声到了,吓了大家一跳,嘿嘿笑道:"别怕别怕,是我啊"`,
|
||||
`${gender}<b>${uname}</b>骑着白马翩翩而来,英姿飒爽,拱手道:"江湖路远,各位有礼了"`,
|
||||
`${gender}<b>${uname}</b>开着宝马一路飞驰到此,推开车门走了下来,向大家挥了挥手`,
|
||||
`${gender}<b>${uname}</b>踩着风火轮呼啸而至,在人群中潇洒亮相`,
|
||||
`${gender}<b>${uname}</b>乘坐滑翔伞从天空缓缓降落,对大家喊道:"hello,我从天上来!"`,
|
||||
`${gender}<b>${uname}</b>从地下钻了出来,拍了拍土,说:"哎呀,走错路了,不过总算到了"`,
|
||||
`${gender}<b>${uname}</b>蹦蹦跳跳地跑了进来,嘻嘻哈哈地跟大家打招呼`,
|
||||
`${gender}<b>${uname}</b>悄悄地溜了进来,生怕被人发现,东张西望了一番`,
|
||||
`${gender}<b>${uname}</b>迈着六亲不认的步伐走进来,气场两米八`,
|
||||
];
|
||||
const msg = welcomeTemplates[Math.floor(Math.random() * welcomeTemplates.length)];
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
const sysDiv = document.createElement('div');
|
||||
sysDiv.className = 'msg-line';
|
||||
sysDiv.innerHTML =
|
||||
`<span style="color: green">【欢迎】${msg}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
container.appendChild(sysDiv);
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
window.addEventListener('chat:leaving', (e) => {
|
||||
const user = e.detail;
|
||||
delete onlineUsers[user.username];
|
||||
renderUserList();
|
||||
|
||||
const sysDiv = document.createElement('div');
|
||||
sysDiv.className = 'msg-line sys-msg';
|
||||
sysDiv.innerHTML = `<span style="color: gray">☆ ${user.username} 离开了聊天室 ☆</span>`;
|
||||
container.appendChild(sysDiv);
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
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('rooms.index') }}";
|
||||
}
|
||||
});
|
||||
|
||||
// ── 禁言状态(本地计时器) ──
|
||||
let isMutedUntil = 0;
|
||||
|
||||
window.addEventListener('chat:muted', (e) => {
|
||||
const d = e.detail;
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
// 在聊天窗口显示禁言通知
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg-line';
|
||||
div.innerHTML =
|
||||
`<span style="color: #c00; font-weight: bold;">【系统】${d.message}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
container.appendChild(div);
|
||||
if (autoScroll) container.scrollTop = container.scrollHeight;
|
||||
|
||||
// 如果是自己被禁言,设置本地禁言计时
|
||||
if (d.username === window.chatContext.username && d.mute_time > 0) {
|
||||
isMutedUntil = Date.now() + d.mute_time * 60 * 1000;
|
||||
const contentInput = document.getElementById('content');
|
||||
if (contentInput) {
|
||||
contentInput.placeholder = `您已被禁言 ${d.mute_time} 分钟,解禁后方可发言...`;
|
||||
contentInput.disabled = true;
|
||||
// 到期自动恢复
|
||||
setTimeout(() => {
|
||||
isMutedUntil = 0;
|
||||
contentInput.placeholder = '在这里输入聊天内容,按 Enter 发送...';
|
||||
contentInput.disabled = false;
|
||||
const unmuteDiv = document.createElement('div');
|
||||
unmuteDiv.className = 'msg-line';
|
||||
unmuteDiv.innerHTML =
|
||||
'<span style="color: #16a34a; font-weight: bold;">【系统】您的禁言已解除,可以继续发言了。</span>';
|
||||
container.appendChild(unmuteDiv);
|
||||
if (autoScroll) container.scrollTop = container.scrollHeight;
|
||||
}, d.mute_time * 60 * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('chat:title-updated', (e) => {
|
||||
document.getElementById('room-title-display').innerText = e.detail.title;
|
||||
});
|
||||
|
||||
// ── 发送消息(Enter 发送) ───────────────────────
|
||||
document.getElementById('content').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage(e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
*/
|
||||
async function sendMessage(e) {
|
||||
if (e) e.preventDefault();
|
||||
|
||||
// 前端禁言检查
|
||||
if (isMutedUntil > Date.now()) {
|
||||
const remaining = Math.ceil((isMutedUntil - Date.now()) / 1000);
|
||||
alert(`您正在禁言中,还有 ${remaining} 秒后解禁。`);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 管理操作执行(下拉选择 + 目标用户) ─────────────
|
||||
async function executeAdminAction() {
|
||||
const select = document.getElementById('admin-action-select');
|
||||
if (!select) return;
|
||||
const action = select.value;
|
||||
if (!action) {
|
||||
alert('请先选择一个管理操作。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取聊天对象下拉框中选中的用户作为目标
|
||||
const toUserSelect = document.getElementById('to_user');
|
||||
const targetUser = toUserSelect ? toUserSelect.value : '';
|
||||
|
||||
if (!targetUser || targetUser === '大家') {
|
||||
alert('请先在「对...说」下拉中选择要操作的用户。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 操作名称映射
|
||||
const actionNames = {
|
||||
kick: '踢出',
|
||||
mute: '禁言',
|
||||
ban: '封号',
|
||||
banip: '封IP'
|
||||
};
|
||||
const actionName = actionNames[action] || action;
|
||||
|
||||
// 禁言需要输入时长
|
||||
let duration = 5;
|
||||
if (action === 'mute') {
|
||||
const input = prompt('请输入禁言时长(分钟):', '5');
|
||||
if (!input) return;
|
||||
duration = parseInt(input) || 5;
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
const confirmMsg = action === 'banip' ?
|
||||
`⚠️ 严重操作:确定要封禁 ${targetUser} 的IP地址吗?该用户将被封号+封IP!` :
|
||||
`确定要对 ${targetUser} 执行「${actionName}」操作吗?`;
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
try {
|
||||
const body = {
|
||||
room_id: window.chatContext.roomId
|
||||
};
|
||||
if (action === 'mute') body.duration = duration;
|
||||
|
||||
const res = await fetch(`/user/${encodeURIComponent(targetUser)}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await res.json();
|
||||
alert(data.message);
|
||||
} catch (e) {
|
||||
alert('操作失败:' + e.message);
|
||||
}
|
||||
select.value = ''; // 重置下拉
|
||||
}
|
||||
|
||||
// ── 设置房间公告 ─────────────────────────────────────
|
||||
async function promptAnnouncement() {
|
||||
const currentText = document.getElementById('announcement-text')?.textContent?.trim() || '';
|
||||
const newText = prompt('请输入新的房间公告/祝福语:', currentText);
|
||||
if (newText === null || newText.trim() === '') return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/room/${window.chatContext.roomId}/announcement`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
announcement: newText.trim()
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
const marquee = document.getElementById('announcement-text');
|
||||
if (marquee) marquee.textContent = newText.trim();
|
||||
alert('公告已更新!');
|
||||
} else {
|
||||
alert(data.message || '更新失败');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('设置公告失败:' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 滚屏开关 ─────────────────────────────────────
|
||||
function toggleAutoScroll() {
|
||||
autoScroll = !autoScroll;
|
||||
const cb = document.getElementById('auto_scroll');
|
||||
if (cb) cb.checked = autoScroll;
|
||||
const statusEl = document.getElementById('scroll-status');
|
||||
if (statusEl) statusEl.textContent = autoScroll ? '开' : '关';
|
||||
}
|
||||
|
||||
// ── 退出房间 ─────────────────────────────────────
|
||||
async function leaveRoom() {
|
||||
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.close();
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// ── 掉线检测计数器 ──
|
||||
let heartbeatFailCount = 0;
|
||||
const MAX_HEARTBEAT_FAILS = 3;
|
||||
|
||||
// ── 存点功能(手动 + 自动)─────────────────────
|
||||
async function saveExp(silent = false) {
|
||||
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'
|
||||
}
|
||||
});
|
||||
|
||||
// 检测登录态失效
|
||||
if (response.status === 401 || response.status === 419) {
|
||||
alert('⚠️ 您的登录已失效(可能超时或在其他设备登录),请重新登录。');
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok && data.status === 'success') {
|
||||
heartbeatFailCount = 0;
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
const d = data.data;
|
||||
const levelTitle = d.user_level >= 10 ? '管理员' : (d.user_level >= 5 ? '高级会员' : (d
|
||||
.user_level >= 3 ?
|
||||
'三级会员' : '普通会员'));
|
||||
|
||||
let levelInfo = '';
|
||||
if (d.is_max_level) {
|
||||
levelInfo = `级别(${d.user_level});累积经验(${d.exp_num});已满级。`;
|
||||
} else {
|
||||
const requiredExp = d.user_level * d.user_level * 10;
|
||||
const remaining = Math.max(0, requiredExp - d.exp_num);
|
||||
levelInfo = `级别(${d.user_level});累积经验(${d.exp_num});还有(${remaining})升级。`;
|
||||
}
|
||||
|
||||
if (data.data.leveled_up) {
|
||||
const upDiv = document.createElement('div');
|
||||
upDiv.className = 'msg-line';
|
||||
upDiv.innerHTML =
|
||||
`<span style="color: #d97706; font-weight: bold;">【系统】恭喜!你的经验值已达 ${d.exp_num},等级突破至 LV.${d.user_level}!🌟</span><span class="msg-time">(${timeStr})</span>`;
|
||||
container2.appendChild(upDiv);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
const detailDiv = document.createElement('div');
|
||||
detailDiv.className = 'msg-line';
|
||||
detailDiv.innerHTML =
|
||||
`<span style="color: green;">【${levelTitle}存点】您的最新情况:${levelInfo}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
container2.appendChild(detailDiv);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('存点失败', e);
|
||||
heartbeatFailCount++;
|
||||
|
||||
if (heartbeatFailCount >= MAX_HEARTBEAT_FAILS) {
|
||||
alert('⚠️ 与服务器的连接已断开,请检查网络后重新登录。');
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
const sysDiv = document.createElement('div');
|
||||
sysDiv.className = 'msg-line';
|
||||
sysDiv.innerHTML = `<span style="color: red;">【系统】存点失败,请稍后重试</span>`;
|
||||
container2.appendChild(sysDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 自动存点心跳(每60秒自动存一次)───────────
|
||||
const HEARTBEAT_INTERVAL = 60 * 1000;
|
||||
setInterval(() => saveExp(true), HEARTBEAT_INTERVAL);
|
||||
setTimeout(() => saveExp(true), 10000);
|
||||
|
||||
// ── 头像选择器(原版 fw.asp 功能)───────────────
|
||||
let avatarPickerLoaded = false;
|
||||
|
||||
/**
|
||||
* 打开头像选择弹窗
|
||||
*/
|
||||
function openAvatarPicker() {
|
||||
const modal = document.getElementById('avatar-picker-modal');
|
||||
modal.style.display = 'flex';
|
||||
|
||||
if (!avatarPickerLoaded) {
|
||||
loadHeadfaces();
|
||||
avatarPickerLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭头像选择弹窗
|
||||
*/
|
||||
function closeAvatarPicker() {
|
||||
document.getElementById('avatar-picker-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载头像列表
|
||||
*/
|
||||
async function loadHeadfaces() {
|
||||
const grid = document.getElementById('avatar-grid');
|
||||
grid.innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/headface/list');
|
||||
const data = await res.json();
|
||||
grid.innerHTML = '';
|
||||
|
||||
data.headfaces.forEach(file => {
|
||||
const img = document.createElement('img');
|
||||
img.src = '/images/headface/' + file;
|
||||
img.className = 'avatar-option';
|
||||
img.title = file;
|
||||
img.dataset.file = file;
|
||||
img.onerror = () => img.style.display = 'none';
|
||||
img.onclick = () => selectAvatar(file, img);
|
||||
grid.appendChild(img);
|
||||
});
|
||||
} catch (e) {
|
||||
grid.innerHTML = '<div style="text-align:center;padding:20px;color:red;">加载失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中一个头像
|
||||
*/
|
||||
function selectAvatar(file, imgEl) {
|
||||
document.querySelectorAll('.avatar-option.selected').forEach(el => el.classList.remove('selected'));
|
||||
imgEl.classList.add('selected');
|
||||
document.getElementById('avatar-preview').src = '/images/headface/' + file;
|
||||
document.getElementById('avatar-selected-name').textContent = file;
|
||||
document.getElementById('avatar-save-btn').disabled = false;
|
||||
document.getElementById('avatar-save-btn').dataset.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存选中的头像
|
||||
*/
|
||||
async function saveAvatar() {
|
||||
const btn = document.getElementById('avatar-save-btn');
|
||||
const file = btn.dataset.file;
|
||||
if (!file) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '保存中...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/headface/change', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
headface: file
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
alert('头像修改成功!');
|
||||
const myName = window.chatContext.username;
|
||||
if (onlineUsers[myName]) {
|
||||
onlineUsers[myName].headface = data.headface;
|
||||
}
|
||||
renderUserList();
|
||||
closeAvatarPicker();
|
||||
} else {
|
||||
alert(data.message || '修改失败');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('网络错误');
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = '确定更换';
|
||||
}
|
||||
// ── 钓鱼小游戏(复刻原版 diaoyu/ 功能)─────────────
|
||||
let fishingTimer = null;
|
||||
let fishingReelTimeout = null;
|
||||
|
||||
/**
|
||||
* 开始钓鱼 — 调用抛竿 API,花费金币,显示等待动画
|
||||
*/
|
||||
async function startFishing() {
|
||||
const btn = document.getElementById('fishing-btn');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(window.chatContext.fishCastUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.status !== 'success') {
|
||||
alert(data.message || '钓鱼失败');
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 在包厢窗口显示抛竿消息
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
const castDiv = document.createElement('div');
|
||||
castDiv.className = 'msg-line';
|
||||
castDiv.innerHTML =
|
||||
`<span style="color: #2563eb; font-weight: bold;">🎣【钓鱼】</span>${data.message}<span class="msg-time">(${timeStr})</span>`;
|
||||
container2.appendChild(castDiv);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
|
||||
// 等待鱼上钩(后端返回的随机等待秒数)
|
||||
btn.textContent = '🎣 等待中...';
|
||||
|
||||
fishingTimer = setTimeout(() => {
|
||||
// 鱼上钩了!
|
||||
const hookDiv = document.createElement('div');
|
||||
hookDiv.className = 'msg-line';
|
||||
hookDiv.innerHTML =
|
||||
'<span style="color: #d97706; font-weight: bold; font-size: 14px;">🐟 鱼上钩了!快点击 [拉竿] 按钮!</span>';
|
||||
container2.appendChild(hookDiv);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
|
||||
btn.textContent = '🎣 拉竿!';
|
||||
btn.disabled = false;
|
||||
btn.onclick = reelFish;
|
||||
|
||||
// 15 秒内不拉竿,鱼跑掉
|
||||
fishingReelTimeout = setTimeout(() => {
|
||||
const missDiv = document.createElement('div');
|
||||
missDiv.className = 'msg-line';
|
||||
missDiv.innerHTML =
|
||||
'<span style="color: #999;">💨 你反应太慢了,鱼跑掉了...</span>';
|
||||
container2.appendChild(missDiv);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
|
||||
resetFishingBtn();
|
||||
}, 15000);
|
||||
}, data.wait_time * 1000);
|
||||
|
||||
} catch (e) {
|
||||
alert('网络错误:' + e.message);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉竿 — 调用收竿 API,获取随机结果
|
||||
*/
|
||||
async function reelFish() {
|
||||
const btn = document.getElementById('fishing-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '🎣 拉竿中...';
|
||||
|
||||
// 取消跑鱼计时器
|
||||
if (fishingReelTimeout) {
|
||||
clearTimeout(fishingReelTimeout);
|
||||
fishingReelTimeout = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(window.chatContext.fishReelUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
if (res.ok && data.status === 'success') {
|
||||
const r = data.result;
|
||||
const color = r.exp >= 0 ? '#16a34a' : '#dc2626';
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'msg-line';
|
||||
resultDiv.innerHTML =
|
||||
`<span style="color: ${color}; font-weight: bold;">${r.emoji}【钓鱼结果】</span>${r.message}` +
|
||||
` <span style="color: #666; font-size: 11px;">(当前经验:${data.exp_num} 金币:${data.jjb})</span>` +
|
||||
`<span class="msg-time">(${timeStr})</span>`;
|
||||
container2.appendChild(resultDiv);
|
||||
} else {
|
||||
const errDiv = document.createElement('div');
|
||||
errDiv.className = 'msg-line';
|
||||
errDiv.innerHTML =
|
||||
`<span style="color: red;">【钓鱼】${data.message || '操作失败'}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
container2.appendChild(errDiv);
|
||||
}
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
} catch (e) {
|
||||
alert('网络错误:' + e.message);
|
||||
}
|
||||
|
||||
resetFishingBtn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置钓鱼按钮状态
|
||||
*/
|
||||
function resetFishingBtn() {
|
||||
const btn = document.getElementById('fishing-btn');
|
||||
btn.textContent = '🎣 钓鱼';
|
||||
btn.disabled = false;
|
||||
btn.onclick = startFishing;
|
||||
fishingTimer = null;
|
||||
fishingReelTimeout = null;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
{{--
|
||||
文件功能:聊天室竖向工具条(中间导航栏)
|
||||
从 frame.blade.php 拆分,便于独立维护
|
||||
|
||||
依赖变量:$user, $superLevel, $room
|
||||
--}}
|
||||
|
||||
<div class="chat-toolbar" id="toolbar-strip">
|
||||
@if ($user->user_level >= $superLevel)
|
||||
<div class="tool-btn" style="color: #ffcc00;" onclick="window.open('/admin', '_blank')" title="管理后台">
|
||||
管理人员</div>
|
||||
@endif
|
||||
<div class="tool-btn" onclick="window.open('{{ route('rooms.index') }}', '_blank')" title="商店">商店
|
||||
</div>
|
||||
<div class="tool-btn" onclick="saveExp()" title="手动存经验点">存点</div>
|
||||
<div class="tool-btn" onclick="window.open('{{ route('leaderboard.index') }}', '_blank')" title="排行/娱乐">
|
||||
娱乐
|
||||
</div>
|
||||
<div class="tool-btn" onclick="window.open('{{ route('guestbook.index') }}', '_blank')" title="银行">
|
||||
银行
|
||||
</div>
|
||||
<div class="tool-btn" onclick="switchTab('users')" title="呼叫在线用户">呼叫</div>
|
||||
<div class="tool-btn" onclick="openAvatarPicker()" title="修改头像">头像
|
||||
</div>
|
||||
<div class="tool-btn" onclick="window.open('{{ route('guestbook.index') }}', '_blank')" title="提议/建议">
|
||||
提议</div>
|
||||
<div class="tool-btn" onclick="window.open('{{ route('guestbook.index') }}', '_blank')" title="留言板/私信">
|
||||
留言</div>
|
||||
<div class="tool-btn" onclick="window.open('/admin', '_blank')" title="管理">管理</div>
|
||||
<div class="tool-btn" style="color: #ffaaaa;" onclick="if(confirm('确定要离开聊天室吗?')){leaveRoom();}" title="离开聊天室">离开
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,235 +1,207 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
{{--
|
||||
文件功能:星光留言板页面(含公共留言、收件箱、发件箱三个分类)
|
||||
支持公开留言和悄悄话私信功能
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>星光留言板 - 飘落流星</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
@extends layouts.app
|
||||
--}}
|
||||
@extends('layouts.app')
|
||||
|
||||
<body class="bg-gray-50 flex h-screen overflow-hidden text-sm">
|
||||
<div class="h-screen w-full flex flex-col bg-gray-50 overflow-hidden font-sans" x-data="{ showWriteForm: false, towho: '{{ $defaultTo }}' }">
|
||||
@section('title', '星光留言板 - 飘落流星')
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<header class="bg-indigo-900 border-b border-indigo-800 text-white shadow-md relative z-20 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 左侧:标题与返回大厅 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ route('rooms.index') }}"
|
||||
class="text-indigo-200 hover:text-white transition flex items-center group">
|
||||
<svg class="w-5 h-5 mr-1 transform group-hover:-translate-x-1 transition" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回大厅
|
||||
</a>
|
||||
<div class="h-6 w-px bg-indigo-700"></div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xl">✉️</span>
|
||||
<h1 class="text-xl font-extrabold tracking-wider">星光留言板</h1>
|
||||
</div>
|
||||
@section('nav-icon', '✉️')
|
||||
@section('nav-title', '星光留言板')
|
||||
|
||||
@section('nav-left')
|
||||
<a href="{{ route('rooms.index') }}" class="text-indigo-200 hover:text-white transition flex items-center group">
|
||||
<svg class="w-5 h-5 mr-1 transform group-hover:-translate-x-1 transition" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回大厅
|
||||
</a>
|
||||
@endsection
|
||||
|
||||
@section('nav-right')
|
||||
<button id="write-toggle-btn"
|
||||
@click="showWriteForm = !showWriteForm; if(showWriteForm) setTimeout(() => $refs.textBody.focus(), 100)"
|
||||
class="bg-indigo-500 hover:bg-indigo-400 text-white px-4 py-2 rounded-lg font-bold shadow-sm transition flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z">
|
||||
</path>
|
||||
</svg>
|
||||
<span x-text="showWriteForm ? '取消留言' : '发布留言 / 写信'"></span>
|
||||
</button>
|
||||
@endsection
|
||||
|
||||
@section('body-class', 'flex flex-col h-screen overflow-hidden')
|
||||
@section('body-data', "x-data=\"{ showWriteForm: false, towho: '{{ $defaultTo }}' }\"")
|
||||
|
||||
@section('content')
|
||||
{{-- 验证错误信息 --}}
|
||||
@if (isset($errors) && $errors->any())
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4 mx-4 mt-4 shadow-sm">
|
||||
<ul class="list-disc pl-5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 写信/留言表单区 (Alpine 控制显隐) --}}
|
||||
<div x-show="showWriteForm" x-collapse class="bg-white border-b border-gray-200 shadow-inner">
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<form action="{{ route('guestbook.store') }}" method="POST" class="space-y-4">
|
||||
@csrf
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">接收人 (留空或填"大家"表示公共留言)</label>
|
||||
<input type="text" name="towho" x-model="towho" placeholder="系统自动处理"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
<div class="flex items-center h-full pt-6">
|
||||
<label
|
||||
class="flex items-center space-x-2 text-sm text-gray-700 cursor-pointer bg-pink-50 px-3 py-2 rounded-md hover:bg-pink-100 transition border border-pink-100">
|
||||
<input type="checkbox" name="secret" value="1"
|
||||
class="rounded text-pink-500 focus:ring-pink-500 w-4 h-4">
|
||||
<span class="font-bold text-pink-700">🔒 悄悄话 (仅双方可见)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:写留言按钮 -->
|
||||
<button
|
||||
@click="showWriteForm = !showWriteForm; if(showWriteForm) setTimeout(() => $refs.textBody.focus(), 100)"
|
||||
class="bg-indigo-500 hover:bg-indigo-400 text-white px-4 py-2 rounded-lg font-bold shadow-sm transition flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">正文内容 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<textarea name="text_body" x-ref="textBody" rows="3" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
placeholder="相逢何必曾相识,留下您的足迹吧..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-6 rounded-md shadow flex items-center font-bold">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z">
|
||||
</path>
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
|
||||
</svg>
|
||||
<span x-text="showWriteForm ? '取消留言' : '发布留言 / 写信'"></span>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (session('success'))
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-4 mx-4 mt-4 shadow-sm"
|
||||
role="alert">
|
||||
<p>{{ session('success') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4 mx-4 mt-4 shadow-sm" role="alert">
|
||||
<p>{{ session('error') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if (isset($errors) && $errors->any())
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4 mx-4 mt-4 shadow-sm">
|
||||
<ul class="list-disc pl-5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 写信/留言表单区 (Alpine 控制显隐) -->
|
||||
<div x-show="showWriteForm" x-collapse class="bg-white border-b border-gray-200 shadow-inner">
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<form action="{{ route('guestbook.store') }}" method="POST" class="space-y-4">
|
||||
@csrf
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">接收人 (留空或填“大家”表示公共留言)</label>
|
||||
<input type="text" name="towho" x-model="towho" placeholder="系统自动处理"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
<div class="flex items-center h-full pt-6">
|
||||
<label
|
||||
class="flex items-center space-x-2 text-sm text-gray-700 cursor-pointer bg-pink-50 px-3 py-2 rounded-md hover:bg-pink-100 transition border border-pink-100">
|
||||
<input type="checkbox" name="secret" value="1"
|
||||
class="rounded text-pink-500 focus:ring-pink-500 w-4 h-4">
|
||||
<span class="font-bold text-pink-700">🔒 悄悄话 (仅双方可见)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">正文内容 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<textarea name="text_body" x-ref="textBody" rows="3" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
placeholder="相逢何必曾相识,留下您的足迹吧..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-6 rounded-md shadow flex items-center font-bold">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
|
||||
</svg>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
|
||||
<!-- 左侧:分类导航 -->
|
||||
<div class="w-64 bg-white border-r border-gray-200 shrink-0 hidden md:block">
|
||||
<div class="p-6">
|
||||
<nav class="space-y-2">
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'public']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'public' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">🌍</span> 公共留言墙
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'inbox']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'inbox' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">📥</span> 我收件的
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'outbox']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'outbox' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">📤</span> 我发出的
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:留言流列表 -->
|
||||
<main class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto space-y-4">
|
||||
|
||||
@forelse($messages as $msg)
|
||||
@php
|
||||
// 判断是否属于自己发或收的悄悄话,用于高亮
|
||||
$isSecret = $msg->secret == 1;
|
||||
$isToMe = Auth::check() && $msg->towho === Auth::user()->username;
|
||||
$isFromMe = Auth::check() && $msg->who === Auth::user()->username;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border {{ $isSecret ? 'border-pink-200' : 'border-gray-200' }} p-5 relative group overflow-hidden">
|
||||
|
||||
<!-- 悄悄话角标 -->
|
||||
@if ($isSecret)
|
||||
<div
|
||||
class="absolute top-0 right-0 bg-pink-100 text-pink-700 text-[10px] font-bold px-2 py-1 rounded-bl-lg">
|
||||
🔒 私密信件
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="font-bold text-indigo-700">{{ $msg->who }}</span>
|
||||
<span class="text-gray-400 mx-2">给</span>
|
||||
<span
|
||||
class="font-bold {{ $msg->towho ? 'text-indigo-700' : 'text-gray-500' }}">{{ $msg->towho ?: '大家' }}</span>
|
||||
<span class="text-gray-400 mx-2">留言:</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 flex items-center space-x-3">
|
||||
<span>{{ \Carbon\Carbon::parse($msg->post_time)->diffForHumans() }}</span>
|
||||
|
||||
<!-- 删除按钮 (只有发件人、收件人、超管可见) -->
|
||||
@if ($isFromMe || $isToMe || (Auth::check() && Auth::user()->user_level >= 15))
|
||||
<form action="{{ route('guestbook.destroy', $msg->id) }}" method="POST"
|
||||
onsubmit="return confirm('确定要抹除这条留言吗?');" class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="text-red-400 hover:text-red-600 transition opacity-0 group-hover:opacity-100 bg-red-50 px-2 py-1 rounded">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-gray-800 leading-relaxed text-sm whitespace-pre-wrap {{ $isSecret ? 'bg-pink-50 p-3 rounded-lg border border-pink-100' : 'bg-gray-50 p-3 rounded-lg border border-gray-100' }}">
|
||||
{!! nl2br(e($msg->text_body)) !!}
|
||||
</div>
|
||||
|
||||
<!-- 快捷回复按钮 -->
|
||||
@if (!Auth::check() || $msg->who !== Auth::user()->username)
|
||||
<div class="mt-3 flex justify-end">
|
||||
<button
|
||||
@click="showWriteForm = true; towho = '{{ $msg->who }}'; setTimeout(() => $refs.textBody.focus(), 100); window.scrollTo({top:0, behavior:'smooth'})"
|
||||
class="text-xs text-indigo-500 hover:text-indigo-700 font-medium flex items-center transition">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
|
||||
</svg>
|
||||
回复TA
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<span class="text-4xl">📭</span>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">暂无信件</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">这里是空空如也的荒原。</p>
|
||||
<button
|
||||
@click="showWriteForm = true; towho = ''; setTimeout(() => $refs.textBody.focus(), 100)"
|
||||
class="mt-4 text-indigo-600 font-bold hover:underline">
|
||||
来抢沙发留言吧!
|
||||
</button>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mt-6">
|
||||
{{ $messages->links() }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端底部分类栏 -->
|
||||
{{-- 主体内容区 --}}
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
|
||||
{{-- 左侧:分类导航 --}}
|
||||
<div class="w-64 bg-white border-r border-gray-200 shrink-0 hidden md:block">
|
||||
<div class="p-6">
|
||||
<nav class="space-y-2">
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'public']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'public' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">🌍</span> 公共留言墙
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'inbox']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'inbox' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">📥</span> 我收件的
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'outbox']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'outbox' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">📤</span> 我发出的
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 右侧:留言流列表 --}}
|
||||
<main class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto space-y-4">
|
||||
|
||||
@forelse($messages as $msg)
|
||||
@php
|
||||
$isSecret = $msg->secret == 1;
|
||||
$isToMe = Auth::check() && $msg->towho === Auth::user()->username;
|
||||
$isFromMe = Auth::check() && $msg->who === Auth::user()->username;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border {{ $isSecret ? 'border-pink-200' : 'border-gray-200' }} p-5 relative group overflow-hidden">
|
||||
|
||||
@if ($isSecret)
|
||||
<div
|
||||
class="absolute top-0 right-0 bg-pink-100 text-pink-700 text-[10px] font-bold px-2 py-1 rounded-bl-lg">
|
||||
🔒 私密信件
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="font-bold text-indigo-700">{{ $msg->who }}</span>
|
||||
<span class="text-gray-400 mx-2">给</span>
|
||||
<span
|
||||
class="font-bold {{ $msg->towho ? 'text-indigo-700' : 'text-gray-500' }}">{{ $msg->towho ?: '大家' }}</span>
|
||||
<span class="text-gray-400 mx-2">留言:</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 flex items-center space-x-3">
|
||||
<span>{{ \Carbon\Carbon::parse($msg->post_time)->diffForHumans() }}</span>
|
||||
|
||||
@if ($isFromMe || $isToMe || (Auth::check() && Auth::user()->user_level >= 15))
|
||||
<form action="{{ route('guestbook.destroy', $msg->id) }}" method="POST"
|
||||
onsubmit="return confirm('确定要抹除这条留言吗?');" class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="text-red-400 hover:text-red-600 transition opacity-0 group-hover:opacity-100 bg-red-50 px-2 py-1 rounded">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-gray-800 leading-relaxed text-sm whitespace-pre-wrap {{ $isSecret ? 'bg-pink-50 p-3 rounded-lg border border-pink-100' : 'bg-gray-50 p-3 rounded-lg border border-gray-100' }}">
|
||||
{!! nl2br(e($msg->text_body)) !!}
|
||||
</div>
|
||||
|
||||
@if (!Auth::check() || $msg->who !== Auth::user()->username)
|
||||
<div class="mt-3 flex justify-end">
|
||||
<button
|
||||
@click="showWriteForm = true; towho = '{{ $msg->who }}'; setTimeout(() => $refs.textBody.focus(), 100); window.scrollTo({top:0, behavior:'smooth'})"
|
||||
class="text-xs text-indigo-500 hover:text-indigo-700 font-medium flex items-center transition">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
|
||||
</svg>
|
||||
回复TA
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<span class="text-4xl">📭</span>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">暂无信件</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">这里是空空如也的荒原。</p>
|
||||
<button @click="showWriteForm = true; towho = ''; setTimeout(() => $refs.textBody.focus(), 100)"
|
||||
class="mt-4 text-indigo-600 font-bold hover:underline">
|
||||
来抢沙发留言吧!
|
||||
</button>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
{{-- 分页 --}}
|
||||
<div class="mt-6">
|
||||
{{ $messages->links() }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{-- 移动端底部分类栏 --}}
|
||||
<div class="md:hidden bg-white border-t border-gray-200 flex justify-around p-2 shrink-0 relative z-20">
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'public']) }}"
|
||||
class="flex flex-col items-center {{ $tab === 'public' ? 'text-indigo-600' : 'text-gray-500' }}">
|
||||
@@ -247,7 +219,4 @@
|
||||
<span class="text-xs mt-1">发件箱</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@endsection
|
||||
|
||||
+406
-67
@@ -1,84 +1,422 @@
|
||||
{{--
|
||||
文件功能:聊天室登录/注册首页(复刻原版 DEFAULT.asp 风格)
|
||||
包含房间选择、性别选择,登录后弹出独立窗口直接进入聊天
|
||||
--}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '飘落的流星在线聊天' }} - 登录</title>
|
||||
<!-- 使用现代 CDN 引入 Tailwind CSS (快速构建 UI) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<title>{{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '飘落的流星在线聊天' }}</title>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "宋体", SimSun, "Microsoft YaHei", sans-serif;
|
||||
font-size: 12px;
|
||||
background: #c0d8ef;
|
||||
color: #333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 居中卡片 */
|
||||
.login-card {
|
||||
width: 720px;
|
||||
border: 1px solid #8aaccf;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
box-shadow: 2px 3px 12px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.card-header {
|
||||
background: linear-gradient(180deg, #3a6fa0, #2a5580);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 12px 0 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 3px;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 公告条 */
|
||||
.notice-bar {
|
||||
background: #fffde0;
|
||||
border-bottom: 1px solid #e8d88e;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
color: #996600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 内容区:左右分栏 */
|
||||
.card-body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 左侧:登录表单 */
|
||||
.login-left {
|
||||
flex: 1;
|
||||
background: #eef5ff;
|
||||
padding: 24px 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #2a5580;
|
||||
border-bottom: 1px solid #b8d0e8;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #336699;
|
||||
font-weight: bold;
|
||||
padding-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 5px 8px;
|
||||
height: 32px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #336699;
|
||||
outline: none;
|
||||
box-shadow: 0 0 3px rgba(51, 102, 153, 0.3);
|
||||
}
|
||||
|
||||
.gender-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 6px 0 14px;
|
||||
padding-left: 58px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gender-row label {
|
||||
cursor: pointer;
|
||||
color: #336699;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.captcha-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.captcha-wrap input {
|
||||
width: 80px;
|
||||
flex: none;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.captcha-wrap img {
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 24px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(180deg, #f8f8f8, #e0e0e0);
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: linear-gradient(180deg, #e8e8e8, #d0d0d0);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(180deg, #4a8cc5, #336699);
|
||||
color: #fff;
|
||||
border-color: #2a5580;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(180deg, #3a7cb5, #2a5a88);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.link-row {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert {
|
||||
display: none;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fde8e8;
|
||||
color: #cc0000;
|
||||
border: 1px solid #f5c0c0;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #e8fde8;
|
||||
color: #006600;
|
||||
border: 1px solid #c0f5c0;
|
||||
}
|
||||
|
||||
/* 右侧:房间列表 */
|
||||
.login-right {
|
||||
width: 250px;
|
||||
background: #f0f6ff;
|
||||
border-left: 1px solid #b8d0e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.room-header {
|
||||
background: #d8e8f5;
|
||||
color: #336699;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 7px 10px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #b8d0e8;
|
||||
}
|
||||
|
||||
.room-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.room-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #e0ecf7;
|
||||
font-size: 12px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.room-item:hover {
|
||||
background: #dde8ff;
|
||||
}
|
||||
|
||||
.room-item.selected {
|
||||
background: #d0e4f5;
|
||||
color: #1a4a70;
|
||||
}
|
||||
|
||||
.room-item input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.room-owner {
|
||||
font-size: 11px;
|
||||
color: #8aaccf;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.room-item.selected .room-owner {
|
||||
color: #b8d0e8;
|
||||
}
|
||||
|
||||
.room-footer {
|
||||
background: #e0ecf7;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
border-top: 1px solid #b8d0e8;
|
||||
}
|
||||
|
||||
/* 底部版权 */
|
||||
.card-footer {
|
||||
text-align: center;
|
||||
padding: 6px;
|
||||
font-size: 11px;
|
||||
color: #7a9fc0;
|
||||
background: #e0ecf7;
|
||||
border-top: 1px solid #b8d0e8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 h-screen flex items-center justify-center">
|
||||
<body>
|
||||
<div class="login-card">
|
||||
{{-- 标题 --}}
|
||||
<div class="card-header">
|
||||
{{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '飘落的流星在线聊天' }}
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-6 text-gray-800">
|
||||
{{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '在线聊天室' }}
|
||||
</h1>
|
||||
{{-- 公告 --}}
|
||||
<div class="notice-bar">
|
||||
{{ \App\Models\SysParam::where('alias', 'sys_notice')->value('body') ?? '欢迎来到聊天室!第一次登录即为注册,请记住您的密码。' }}
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 text-center mb-6">
|
||||
{{ \App\Models\SysParam::where('alias', 'sys_notice')->value('body') ?? '欢迎您的加入' }}
|
||||
</p>
|
||||
{{-- 内容区 --}}
|
||||
<form id="login-form">
|
||||
<div class="card-body">
|
||||
{{-- 左侧:登录表单 --}}
|
||||
<div class="login-left">
|
||||
<div class="section-title">🔑 用户登录</div>
|
||||
<div id="alert-box" class="alert"></div>
|
||||
|
||||
<!-- 登录提示区 -->
|
||||
<div id="alert-box" class="hidden mb-4 p-3 rounded text-sm text-center"></div>
|
||||
|
||||
<form id="login-form" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">昵称 (第一次登录即注册)</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="允许中英文、数字、下划线">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">密码</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="请输入密码">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="captcha" class="block text-sm font-medium text-gray-700">验证码</label>
|
||||
<div class="mt-1 flex space-x-2">
|
||||
<input type="text" id="captcha" name="captcha" required
|
||||
class="block w-2/3 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="输入右侧字符">
|
||||
<div class="w-1/3 cursor-pointer" onclick="refreshCaptcha()">
|
||||
<!-- 验证码图片,点击刷新 -->
|
||||
<img src="{{ captcha_src() }}" alt="验证码" id="captcha-img"
|
||||
class="h-full w-full rounded-md border border-gray-300 object-cover">
|
||||
<div class="form-row">
|
||||
<span class="form-label">昵称:</span>
|
||||
<input type="text" id="username" name="username" class="form-input" maxlength="10"
|
||||
placeholder="中英文、数字、下划线" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<span class="form-label">密码:</span>
|
||||
<input type="password" id="password" name="password" class="form-input" maxlength="20"
|
||||
placeholder="请输入密码" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<span class="form-label">验证:</span>
|
||||
<div class="captcha-wrap">
|
||||
<input type="text" id="captcha" name="captcha" class="form-input" maxlength="10"
|
||||
placeholder="输入验证码" required>
|
||||
<img src="/captcha/default?{{ mt_rand() }}" alt="验证码" id="captcha-img"
|
||||
onclick="refreshCaptcha()" title="点击刷新">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gender-row">
|
||||
<label><input type="radio" name="bSex" value="1" checked> 男</label>
|
||||
<label><input type="radio" name="bSex" value="2"> 女</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button type="submit" id="submit-btn" class="btn btn-primary">进入聊天</button>
|
||||
<button type="reset" class="btn">重填</button>
|
||||
</div>
|
||||
|
||||
<div class="link-row">第一次登录即为注册,请记住您的密码</div>
|
||||
</div>
|
||||
|
||||
{{-- 右侧:房间列表 --}}
|
||||
<div class="login-right">
|
||||
<div class="room-header">📋 选择房间</div>
|
||||
<div class="room-list">
|
||||
@forelse ($rooms as $room)
|
||||
<label class="room-item {{ $loop->first ? 'selected' : '' }}">
|
||||
<input type="radio" name="room_id" value="{{ $room->id }}"
|
||||
{{ $loop->first ? 'checked' : '' }}>
|
||||
<div class="room-info">
|
||||
<span class="room-name">{{ $room->name }}</span>
|
||||
@if ($room->master)
|
||||
<span class="room-owner">房主:{{ $room->master }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</label>
|
||||
@empty
|
||||
<div style="padding: 20px; text-align: center; color: #999;">暂无房间</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="room-footer">共 {{ count($rooms) }} 个房间</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submit-btn"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50">
|
||||
进入聊天室
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{-- 底部 --}}
|
||||
<div class="card-footer">Powered by 飘落的流星 © {{ date('Y') }}</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 刷新验证码
|
||||
function refreshCaptcha() {
|
||||
document.getElementById('captcha-img').src = '{{ captcha_src() }}' + Math.random();
|
||||
document.getElementById('captcha-img').src = '/captcha/default?' + Math.random();
|
||||
}
|
||||
|
||||
// 提交登录表单
|
||||
document.querySelectorAll('.room-item').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
document.querySelectorAll('.room-item').forEach(i => i.classList.remove('selected'));
|
||||
this.classList.add('selected');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('submit-btn');
|
||||
const alertBox = document.getElementById('alert-box');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerText = '正在进入...';
|
||||
alertBox.classList.add('hidden');
|
||||
alertBox.style.display = 'none';
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
const roomId = data.room_id || '1';
|
||||
|
||||
fetch('{{ route('login.post') }}', {
|
||||
method: 'POST',
|
||||
@@ -90,27 +428,33 @@
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json().then(data => ({
|
||||
.then(response => response.json().then(body => ({
|
||||
status: response.status,
|
||||
body: data
|
||||
body
|
||||
})))
|
||||
.then(result => {
|
||||
if (result.status === 200 && result.body.status === 'success') {
|
||||
// 登录成功,显示成功并跳转
|
||||
showAlert(result.body.message, 'success');
|
||||
const chatUrl = '/room/' + roomId;
|
||||
const chatWin = window.open(chatUrl, 'chatroom',
|
||||
'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes'
|
||||
);
|
||||
if (chatWin) {
|
||||
chatWin.moveTo(0, 0);
|
||||
chatWin.resizeTo(screen.availWidth, screen.availHeight);
|
||||
chatWin.focus();
|
||||
}
|
||||
setTimeout(() => {
|
||||
// 转跳到大厅房间列表
|
||||
window.location.href = '/rooms';
|
||||
}, 1000);
|
||||
}, 1500);
|
||||
} else {
|
||||
// 验证失败或密码错误
|
||||
const errorMsg = result.body.message || (result.body.errors ? Object.values(result.body
|
||||
.errors)[0][0] : '登录失败');
|
||||
const errorMsg = result.body.message ||
|
||||
(result.body.errors ? Object.values(result.body.errors)[0][0] : '登录失败');
|
||||
showAlert(errorMsg, 'error');
|
||||
refreshCaptcha();
|
||||
document.getElementById('captcha').value = '';
|
||||
btn.disabled = false;
|
||||
btn.innerText = '进入聊天室';
|
||||
btn.innerText = '进入聊天';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -118,20 +462,15 @@
|
||||
showAlert('网络或服务器错误,请稍后再试。', 'error');
|
||||
refreshCaptcha();
|
||||
btn.disabled = false;
|
||||
btn.innerText = '进入聊天室';
|
||||
btn.innerText = '进入聊天';
|
||||
});
|
||||
});
|
||||
|
||||
function showAlert(message, type) {
|
||||
const box = document.getElementById('alert-box');
|
||||
box.innerText = message;
|
||||
box.classList.remove('hidden', 'bg-red-100', 'text-red-700', 'bg-green-100', 'text-green-700');
|
||||
|
||||
if (type === 'error') {
|
||||
box.classList.add('bg-red-100', 'text-red-700');
|
||||
} else {
|
||||
box.classList.add('bg-green-100', 'text-green-700');
|
||||
}
|
||||
box.className = 'alert ' + (type === 'error' ? 'alert-error' : 'alert-success');
|
||||
box.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
{{--
|
||||
文件功能:前台统一布局模板
|
||||
提供公共 <head>、顶部导航栏、消息提示、内容区域和底部
|
||||
所有前台页面(除 chat/frame 和 index 登录页外)统一使用此模板
|
||||
|
||||
可用 section:
|
||||
@section('title') - 页面标题
|
||||
@section('head') - 额外 <head> 内容(JS/CSS)
|
||||
@section('body-class') - <body> 额外 class
|
||||
@section('body-data') - <body> 的 x-data 等属性
|
||||
@section('nav-icon') - 导航栏图标 emoji(如 🌟)
|
||||
@section('nav-title') - 导航栏标题文字
|
||||
@section('nav-left') - 导航栏左侧额外内容(如返回按钮)
|
||||
@section('nav-right') - 导航栏右侧操作区
|
||||
@section('content') - 页面主体内容
|
||||
@section('scripts') - 页面底部脚本
|
||||
|
||||
@author ChatRoom Laravel
|
||||
--}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', '飘落流星聊天室')</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<style>
|
||||
/* 通用滚动条美化 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@yield('head')
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 min-h-screen text-gray-800 text-sm @yield('body-class')" @yield('body-data')>
|
||||
|
||||
{{-- ═══════════ 顶部导航栏(统一风格) ═══════════ --}}
|
||||
<header class="bg-indigo-900 border-b border-indigo-800 text-white shadow-md sticky top-0 z-50 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-14">
|
||||
|
||||
{{-- 左侧:返回 + 标题 --}}
|
||||
<div class="flex items-center space-x-4">
|
||||
@hasSection('nav-left')
|
||||
@yield('nav-left')
|
||||
<div class="h-6 w-px bg-indigo-700"></div>
|
||||
@endif
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xl">@yield('nav-icon', '🌟')</span>
|
||||
<h1 class="text-lg font-extrabold tracking-wider">@yield('nav-title', '飘落流星')</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 右侧:操作区 --}}
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
{{-- 公共导航链接 --}}
|
||||
<a href="{{ route('rooms.index') }}"
|
||||
class="text-indigo-200 hover:text-white font-bold flex items-center transition hidden sm:flex">
|
||||
<span class="mr-1">🏠</span> 大厅
|
||||
</a>
|
||||
<a href="{{ route('leaderboard.index') }}"
|
||||
class="text-yellow-400 hover:text-yellow-300 font-bold flex items-center transition hidden sm:flex">
|
||||
<span class="mr-1">🏆</span> 风云榜
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index') }}"
|
||||
class="text-indigo-200 hover:text-white font-bold flex items-center transition hidden sm:flex">
|
||||
<span class="mr-1">✉️</span> 留言板
|
||||
</a>
|
||||
|
||||
@yield('nav-right')
|
||||
|
||||
{{-- 用户信息(通用) --}}
|
||||
@auth
|
||||
<div class="flex items-center space-x-2 ml-2 pl-2 border-l border-indigo-700">
|
||||
<img src="/images/headface/{{ Auth::user()->headface ?? '01.gif' }}"
|
||||
class="w-7 h-7 rounded border border-indigo-500 object-cover bg-white">
|
||||
<span class="font-bold hidden sm:inline">{{ Auth::user()->username }}</span>
|
||||
<span
|
||||
class="bg-white/20 px-1.5 py-0.5 rounded text-xs border border-white/10">LV.{{ Auth::user()->user_level }}</span>
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ═══════════ 全局提示消息 ═══════════ --}}
|
||||
@if (session('success'))
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded shadow-sm">
|
||||
<p class="font-bold">操作成功</p>
|
||||
<p>{{ session('success') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-sm">
|
||||
<p class="font-bold">操作失败</p>
|
||||
<p>{{ session('error') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ═══════════ 页面主体内容 ═══════════ --}}
|
||||
@yield('content')
|
||||
|
||||
{{-- ═══════════ 底部脚本 ═══════════ --}}
|
||||
@yield('scripts')
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,148 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
{{--
|
||||
文件功能:风云排行榜页面
|
||||
展示等级榜、经验榜、财富榜、魅力榜四大排行
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>风云排行榜 - 飘落流星</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
@extends layouts.app
|
||||
--}}
|
||||
@extends('layouts.app')
|
||||
|
||||
<body class="bg-gray-100 flex h-screen overflow-hidden text-sm">
|
||||
<div class="h-screen w-full flex flex-col bg-gray-100 overflow-hidden font-sans">
|
||||
@section('title', '风云排行榜 - 飘落流星')
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<header class="bg-indigo-900 border-b border-indigo-800 text-white shadow-md relative z-20 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 左侧:标题与返回大厅 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ route('rooms.index') }}"
|
||||
class="text-indigo-200 hover:text-white transition flex items-center group">
|
||||
<svg class="w-5 h-5 mr-1 transform group-hover:-translate-x-1 transition" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回大厅
|
||||
</a>
|
||||
<div class="h-6 w-px bg-indigo-700"></div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xl">🏆</span>
|
||||
<h1 class="text-xl font-extrabold tracking-wider">风云排行榜</h1>
|
||||
</div>
|
||||
</div>
|
||||
@section('nav-icon', '🏆')
|
||||
@section('nav-title', '风云排行榜')
|
||||
|
||||
<!-- 右侧:当前用户状态 -->
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
@auth
|
||||
<img src="/images/headface/{{ Auth::user()->headface ?? '01.gif' }}"
|
||||
class="w-8 h-8 rounded border border-indigo-500 object-cover bg-white">
|
||||
<div class="hidden sm:block">
|
||||
<span class="font-bold">{{ Auth::user()->username }}</span>
|
||||
<span class="text-indigo-300 ml-2">LV.{{ Auth::user()->user_level }}</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="hidden sm:block">
|
||||
<span class="text-indigo-300">游客状态</span>
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@section('nav-left')
|
||||
<a href="{{ route('rooms.index') }}" class="text-indigo-200 hover:text-white transition flex items-center group">
|
||||
<svg class="w-5 h-5 mr-1 transform group-hover:-translate-x-1 transition" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回大厅
|
||||
</a>
|
||||
@endsection
|
||||
|
||||
<!-- 说明条 -->
|
||||
<div class="bg-indigo-50 border-b border-indigo-100 py-3 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center text-sm">
|
||||
<p class="text-indigo-800 font-medium">✨ 数据每 <span class="font-bold text-red-500">15分钟</span>
|
||||
自动刷新一次。努力提升自己,让全服铭记你的名字!</p>
|
||||
</div>
|
||||
|
||||
|
||||
@section('content')
|
||||
{{-- 说明条 --}}
|
||||
<div class="bg-indigo-50 border-b border-indigo-100 py-3 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center text-sm">
|
||||
<p class="text-indigo-800 font-medium">✨ 数据每 <span class="font-bold text-red-500">15分钟</span>
|
||||
自动刷新一次。努力提升自己,让全服铭记你的名字!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滚动内容区 -->
|
||||
<main class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
{{-- 滚动内容区 --}}
|
||||
<main class="p-4 sm:p-6 lg:p-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
<!-- 1. 境界榜 (user_level) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-red-600 to-red-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">👑</span> 无上境界榜
|
||||
</h2>
|
||||
<span class="text-xs bg-red-800/40 px-2 py-0.5 rounded">Level</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topLevels,
|
||||
'valueField' => 'user_level',
|
||||
'unit' => '级',
|
||||
'color' => 'text-red-600',
|
||||
])
|
||||
</div>
|
||||
{{-- 1. 境界榜 (user_level) --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-red-600 to-red-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">👑</span> 无上境界榜
|
||||
</h2>
|
||||
<span class="text-xs bg-red-800/40 px-2 py-0.5 rounded">Level</span>
|
||||
</div>
|
||||
|
||||
<!-- 2. 修为榜 (exp_num) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-amber-600 to-amber-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">🔥</span> 苦修经验榜
|
||||
</h2>
|
||||
<span class="text-xs bg-amber-800/40 px-2 py-0.5 rounded">Exp</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topExp,
|
||||
'valueField' => 'exp_num',
|
||||
'unit' => '点',
|
||||
'color' => 'text-amber-600',
|
||||
])
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topLevels,
|
||||
'valueField' => 'user_level',
|
||||
'unit' => '级',
|
||||
'color' => 'text-red-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 财富榜 (jjb) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-yellow-500 to-yellow-400 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center text-yellow-900"><span
|
||||
class="mr-2 text-xl">💰</span> 盖世神豪榜</h2>
|
||||
<span class="text-xs bg-yellow-800/20 text-yellow-900 px-2 py-0.5 rounded">Coin</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topWealth,
|
||||
'valueField' => 'jjb',
|
||||
'unit' => '万',
|
||||
'color' => 'text-yellow-600',
|
||||
])
|
||||
</div>
|
||||
{{-- 2. 修为榜 (exp_num) --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-amber-600 to-amber-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">🔥</span> 苦修经验榜
|
||||
</h2>
|
||||
<span class="text-xs bg-amber-800/40 px-2 py-0.5 rounded">Exp</span>
|
||||
</div>
|
||||
|
||||
<!-- 4. 魅力榜 (meili) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-pink-600 to-pink-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">🌸</span> 绝世名伶榜
|
||||
</h2>
|
||||
<span class="text-xs bg-pink-800/40 px-2 py-0.5 rounded">Charm</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topCharm,
|
||||
'valueField' => 'meili',
|
||||
'unit' => '点',
|
||||
'color' => 'text-pink-600',
|
||||
])
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topExp,
|
||||
'valueField' => 'exp_num',
|
||||
'unit' => '点',
|
||||
'color' => 'text-amber-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 3. 财富榜 (jjb) --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-yellow-500 to-yellow-400 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center text-yellow-900"><span class="mr-2 text-xl">💰</span>
|
||||
盖世神豪榜</h2>
|
||||
<span class="text-xs bg-yellow-800/20 text-yellow-900 px-2 py-0.5 rounded">Coin</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topWealth,
|
||||
'valueField' => 'jjb',
|
||||
'unit' => '万',
|
||||
'color' => 'text-yellow-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 4. 魅力榜 (meili) --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-pink-600 to-pink-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">🌸</span> 绝世名伶榜
|
||||
</h2>
|
||||
<span class="text-xs bg-pink-800/40 px-2 py-0.5 rounded">Charm</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topCharm,
|
||||
'valueField' => 'meili',
|
||||
'unit' => '点',
|
||||
'color' => 'text-pink-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</div>
|
||||
</main>
|
||||
@endsection
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
{{--
|
||||
文件功能:聊天大厅 - 房间列表页
|
||||
展示所有公开聊天房间,支持创建、编辑、转让、删除房间
|
||||
以及修改个人资料和密码
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>聊天大厅 - 飘落流星</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 引入 Alpine.js 用于简单的无代码弹窗切换 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
@extends layouts.app
|
||||
--}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '聊天大厅 - 飘落流星')
|
||||
|
||||
@section('nav-icon', '🌟')
|
||||
@section('nav-title', '星光大厅')
|
||||
|
||||
@section('head')
|
||||
<style>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@@ -22,102 +27,65 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@endsection
|
||||
|
||||
<body class="bg-gray-100 min-h-screen text-gray-800" x-data="{
|
||||
@section('nav-right')
|
||||
{{-- admin 后台直达 --}}
|
||||
@if (Auth::user()->user_level >= 15)
|
||||
<a href="{{ route('admin.dashboard') }}" class="text-indigo-200 hover:text-white font-bold hidden sm:block">⚙️ 后台</a>
|
||||
@endif
|
||||
{{-- 个人资料 --}}
|
||||
<button @click="showProfileModal = true" class="font-medium text-sm hover:text-indigo-200 transition flex items-center">
|
||||
欢迎您,{{ Auth::user()->username }}
|
||||
</button>
|
||||
{{-- 新建房间 --}}
|
||||
@if (Auth::user()->user_level >= 10)
|
||||
<button @click="showCreateModal = true"
|
||||
class="bg-emerald-500 hover:bg-emerald-400 px-4 py-2 rounded-md font-bold text-sm transition shadow-sm">
|
||||
+ 新建房间
|
||||
</button>
|
||||
@endif
|
||||
{{-- 退出登录 --}}
|
||||
<form action="{{ route('logout') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="text-sm border border-white/30 hover:bg-white/10 px-4 py-2 rounded-md transition font-medium">退出登录</button>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('body-data',
|
||||
"x-data=\"{
|
||||
showCreateModal: false,
|
||||
showEditModal: false,
|
||||
showTransferModal: false,
|
||||
showProfileModal: false,
|
||||
showPasswordModal: false,
|
||||
currentRoom: null
|
||||
}">
|
||||
}\"")
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<nav class="bg-indigo-600 px-6 py-4 shadow-md sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center text-white">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-xl">🌟</span>
|
||||
<h1 class="text-xl font-extrabold tracking-wider">星光大厅</h1>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
|
||||
<!-- 留言板入口 -->
|
||||
<a href="{{ route('guestbook.index') }}"
|
||||
class="text-indigo-100 hover:text-white font-bold flex items-center bg-indigo-800/40 px-3 py-1.5 rounded-full transition shadow-inner">
|
||||
<span class="mr-1">✉️</span> 留言板
|
||||
</a>
|
||||
|
||||
<!-- 风云排行榜入口 -->
|
||||
<a href="{{ route('leaderboard.index') }}"
|
||||
class="mr-4 text-yellow-400 hover:text-yellow-300 font-bold flex items-center bg-indigo-800/50 px-3 py-1.5 rounded-full transition shadow-inner">
|
||||
<span class="mr-1">🏆</span> 风云榜
|
||||
</a>
|
||||
|
||||
<!-- admin 后台直达 -->
|
||||
@if (Auth::user()->user_level >= 15)
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="mr-4 text-indigo-200 hover:text-white font-bold hidden sm:block">⚙️ 后台管理</a>
|
||||
@endif
|
||||
<!-- 点击直接在本页弹出资料卡修改 -->
|
||||
<button @click="showProfileModal = true"
|
||||
class="font-medium text-sm hover:text-indigo-200 transition flex items-center">
|
||||
欢迎您,{{ Auth::user()->username }}
|
||||
<span
|
||||
class="bg-white/20 px-2 py-0.5 rounded-full text-xs ml-2 border border-white/10 shadow-sm">LV.{{ Auth::user()->user_level }}</span>
|
||||
</button>
|
||||
|
||||
{{-- 权限按钮区 --}}
|
||||
@if (Auth::user()->user_level >= 10)
|
||||
<button @click="showCreateModal = true"
|
||||
class="bg-emerald-500 hover:bg-emerald-400 px-4 py-2 rounded-md font-bold text-sm transition shadow-sm">
|
||||
+ 新建房间
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('logout') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="text-sm border border-white/30 hover:bg-white/10 px-4 py-2 rounded-md transition font-medium">退出登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主展示区 -->
|
||||
<main class="max-w-7xl mx-auto py-10 px-6">
|
||||
|
||||
<!-- 全局提示消息 -->
|
||||
@if (session('success'))
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-8 rounded shadow-sm">
|
||||
<p class="font-bold">操作成功</p>
|
||||
<p>{{ session('success') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-8 rounded shadow-sm">
|
||||
<p class="font-bold">发生错误</p>
|
||||
<p>{{ session('error') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-8 rounded shadow-sm">
|
||||
@section('content')
|
||||
{{-- 验证错误信息 --}}
|
||||
@if ($errors->any())
|
||||
<div class="max-w-7xl mx-auto px-6 mt-4">
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-sm">
|
||||
<ul class="list-disc list-inside text-sm">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 主展示区 --}}
|
||||
<main class="max-w-7xl mx-auto py-10 px-6">
|
||||
|
||||
<div class="mb-6 flex justify-between items-end border-b pb-4">
|
||||
<h2 class="text-xl font-bold text-gray-700">公开频段 (<span
|
||||
class="text-indigo-600">{{ $rooms->count() }}</span>)</h2>
|
||||
<h2 class="text-xl font-bold text-gray-700">公开频段 (<span class="text-indigo-600">{{ $rooms->count() }}</span>)
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- 房间瀑布流网格 -->
|
||||
{{-- 房间瀑布流网格 --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
@forelse($rooms as $room)
|
||||
<div
|
||||
@@ -145,7 +113,6 @@
|
||||
{{-- 管理按钮组(仅房主或超管可见) --}}
|
||||
<div class="flex space-x-2">
|
||||
@if ($room->master == Auth::user()->username || Auth::user()->user_level >= 15)
|
||||
<!-- 修改 -->
|
||||
<button
|
||||
@click="currentRoom = {id: {{ $room->id }}, name: '{{ addslashes($room->name) }}', description: '{{ addslashes($room->description) }}'}; showEditModal = true"
|
||||
class="text-xs text-blue-600 hover:text-blue-800 font-semibold px-2 py-1 rounded hover:bg-blue-50 transition">
|
||||
@@ -153,15 +120,12 @@
|
||||
</button>
|
||||
|
||||
@if (!$room->is_system)
|
||||
<!-- 转让 -->
|
||||
<button
|
||||
@click="currentRoom = {id: {{ $room->id }}, name: '{{ addslashes($room->name) }}'}; showTransferModal = true"
|
||||
class="text-xs text-amber-600 hover:text-amber-800 font-semibold px-2 py-1 rounded hover:bg-amber-50 transition">
|
||||
转让
|
||||
</button>
|
||||
<!-- 删除 -->
|
||||
<form action="{{ route('rooms.destroy', $room->id) }}" method="POST"
|
||||
class="inline"
|
||||
<form action="{{ route('rooms.destroy', $room->id) }}" method="POST" class="inline"
|
||||
onsubmit="return confirm('警告:确实要彻底解散「{{ $room->name }}」吗?此操作不可逆!');">
|
||||
@csrf @method('delete')
|
||||
<button type="submit"
|
||||
@@ -172,7 +136,8 @@
|
||||
</div>
|
||||
|
||||
{{-- 进入按钮 --}}
|
||||
<a href="{{ route('chat.room', $room->id) }}"
|
||||
<a href="#"
|
||||
onclick="openChatRoom('{{ route('chat.room', $room->id) }}', '{{ $room->name }}'); return false;"
|
||||
class="bg-indigo-600 text-white hover:bg-indigo-700 px-4 py-2 rounded-t-xl rounded-br-xl text-sm font-bold shadow-md hover:shadow-lg transition-all transform group-hover:-translate-y-0.5">
|
||||
立刻进入 →
|
||||
</a>
|
||||
@@ -187,7 +152,7 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 新建房间 Modal (通过 Alpine.js 开关) -->
|
||||
{{-- ═══════════ 新建房间 Modal ═══════════ --}}
|
||||
<div x-show="showCreateModal" 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="showCreateModal = false"
|
||||
@@ -221,7 +186,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改管理 Modal -->
|
||||
{{-- ═══════════ 修改管理 Modal ═══════════ --}}
|
||||
<div x-show="showEditModal" 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="showEditModal = false"
|
||||
@@ -232,7 +197,6 @@
|
||||
<button @click="showEditModal = false"
|
||||
class="text-blue-400 hover:text-blue-600 font-bold text-xl">×</button>
|
||||
</div>
|
||||
<!-- 注意这里通过 Alpine 动态拼接 action 路径 -->
|
||||
<form :action="'{{ url('rooms') }}/' + currentRoom?.id" method="POST" class="p-6">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-4">
|
||||
@@ -256,7 +220,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 转让房主 Modal -->
|
||||
{{-- ═══════════ 转让房主 Modal ═══════════ --}}
|
||||
<div x-show="showTransferModal" 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="showTransferModal = false"
|
||||
@@ -288,7 +252,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人资料设置 Modal -->
|
||||
{{-- ═══════════ 个人资料设置 Modal ═══════════ --}}
|
||||
<div x-show="showProfileModal" 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="showProfileModal = false"
|
||||
@@ -310,16 +274,28 @@
|
||||
isSaving: false,
|
||||
|
||||
async saveProfile() {
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const res = await fetch('{{ route('user.update_profile') }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), 'Content-Type'
|
||||
: 'application/json' , 'Accept' : 'application/json' }, body: JSON.stringify(this.profileData) }); const
|
||||
data=await res.json(); if (res.ok && data.status === 'success') { alert(data.message);
|
||||
window.location.reload(); } else { alert('保存失败: ' + (data.message || ' 输入有误')); } } catch (e) {
|
||||
alert('网络异常'); } finally { this.isSaving=false; } } }">
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const res = await fetch('{{ route('user.update_profile') }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.profileData)
|
||||
});
|
||||
const
|
||||
data = await res.json();
|
||||
if (res.ok && data.status === 'success') {
|
||||
alert(data.message);
|
||||
window.location.reload();
|
||||
} else { alert('保存失败: ' + (data.message || ' 输入有误')); }
|
||||
} catch (e) {
|
||||
alert('网络异常');
|
||||
} finally { this.isSaving = false; }
|
||||
}
|
||||
}">
|
||||
<form @submit.prevent="saveProfile">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">性别</label>
|
||||
@@ -330,7 +306,6 @@
|
||||
<option value="保密">保密</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 头像选择 (暂时写死输入框,后续可优化为网格选择) -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">头像选择 (01.gif - 50.gif)</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
@@ -363,7 +338,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码 Modal -->
|
||||
{{-- ═══════════ 修改密码 Modal ═══════════ --}}
|
||||
<div x-show="showPasswordModal" style="display: none;"
|
||||
class="fixed inset-0 z-[110] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showPasswordModal = false"
|
||||
@@ -384,25 +359,38 @@
|
||||
isSaving: false,
|
||||
|
||||
async savePassword() {
|
||||
if (this.pwdData.new_password !== this.pwdData.new_password_confirmation) {
|
||||
alert('两次输入的新密码不一致!');
|
||||
return;
|
||||
if (this.pwdData.new_password !== this.pwdData.new_password_confirmation) {
|
||||
alert('两次输入的新密码不一致!');
|
||||
return;
|
||||
}
|
||||
if (this.pwdData.new_password.length < 6) {
|
||||
alert('新密码最少 6 位!');
|
||||
return;
|
||||
}
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const res = await fetch('{{ route('user.update_password') }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.pwdData)
|
||||
});
|
||||
const
|
||||
data = await res.json();
|
||||
if (res.ok && data.status === 'success') {
|
||||
alert(data.message);
|
||||
window.location.href = '{{ route('home') }}';
|
||||
} else {
|
||||
alert('密码修改失败: ' + (data.message || ' 请输入正确的旧密码'));
|
||||
}
|
||||
if (this.pwdData.new_password.length < 6) {
|
||||
alert('新密码最少 6 位!');
|
||||
return;
|
||||
}
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const res = await fetch('{{ route('user.update_password') }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), 'Content-Type'
|
||||
: 'application/json' , 'Accept' : 'application/json' }, body: JSON.stringify(this.pwdData) }); const
|
||||
data=await res.json(); if (res.ok && data.status === 'success') { alert(data.message);
|
||||
window.location.href = '{{ route('home') }}'; // 改密成功重新登录 } else {
|
||||
alert('密码修改失败: ' + (data.message || ' 请输入正确的旧密码')); } } catch (e) { alert('网络异常'); } finally {
|
||||
this.isSaving=false; } } }">
|
||||
} catch (e) { alert('网络异常'); } finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
}
|
||||
}">
|
||||
<form @submit.prevent="savePassword">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">当前旧密码</label>
|
||||
@@ -429,7 +417,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@section('scripts')
|
||||
{{-- 原版风格:弹出独立聊天窗口 --}}
|
||||
<script>
|
||||
/**
|
||||
* 打开聊天室弹出窗口(复刻原版 DEFAULT.asp 的 launchchat 函数)
|
||||
*/
|
||||
function openChatRoom(url, roomName) {
|
||||
var chatWin = window.open(
|
||||
url,
|
||||
'chatroom_' + roomName,
|
||||
'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes'
|
||||
);
|
||||
if (chatWin) {
|
||||
chatWin.moveTo(0, 0);
|
||||
chatWin.resizeTo(screen.availWidth, screen.availHeight);
|
||||
chatWin.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user