feat: 实现挂机修仙、排行榜、大厅重构与全站留言板系统
- (Phase 8) 后台各维度管理与配置 - (Phase 9) 全自动静默挂机修仙升级 - (Phase 9) 四大维度风云排行榜页面 - (Phase 10) 全站留言板与悄悄话私信功能 - 运行 Pint 代码格式化
This commit is contained in:
Vendored
+17
-2
@@ -1,4 +1,19 @@
|
||||
import axios from 'axios';
|
||||
import axios from "axios";
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
|
||||
|
||||
import Echo from "laravel-echo";
|
||||
import Pusher from "pusher-js";
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
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",
|
||||
enabledTransports: ["ws", "wss"],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import "./bootstrap";
|
||||
|
||||
// 这个文件负责处理浏览器与 Laravel Reverb WebSocket 服务器的通信。
|
||||
// 通过 Presence Channel 实现聊天室的核心监听。
|
||||
|
||||
export function initChat(roomId) {
|
||||
if (!roomId) {
|
||||
console.error("未提供 roomId,无法初始化 WebSocket 连接。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加入带有登录人员追踪的 Presence Channel
|
||||
window.Echo.join(`room.${roomId}`)
|
||||
// 当自己成功连接时,获取当前在这里的所有人列表
|
||||
.here((users) => {
|
||||
console.log("当前房间内的在线人员:", users);
|
||||
// 触发自定义事件,让具体的前端 UI 去接管渲染
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chat:here", { detail: users }),
|
||||
);
|
||||
})
|
||||
// 监听其他人的加入
|
||||
.joining((user) => {
|
||||
console.log(user.username + " 进入了房间");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chat:joining", { detail: user }),
|
||||
);
|
||||
})
|
||||
// 监听其他人的离开
|
||||
.leaving((user) => {
|
||||
console.log(user.username + " 离开了房间");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chat:leaving", { detail: user }),
|
||||
);
|
||||
})
|
||||
// 监听新发送的文本消息
|
||||
.listen("MessageSent", (e) => {
|
||||
console.log("收到新发言:", e.message);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chat:message", { detail: e.message }),
|
||||
);
|
||||
})
|
||||
// 监听踢出事件(通常判断是不是自己被踢出了)
|
||||
.listen("UserKicked", (e) => {
|
||||
console.log("踢出通知:", e);
|
||||
window.dispatchEvent(new CustomEvent("chat:kicked", { detail: e }));
|
||||
})
|
||||
// 监听封口禁言事件
|
||||
.listen("UserMuted", (e) => {
|
||||
console.log("禁言通知:", e);
|
||||
window.dispatchEvent(new CustomEvent("chat:muted", { detail: e }));
|
||||
})
|
||||
// 监听房间主题被改变
|
||||
.listen("RoomTitleUpdated", (e) => {
|
||||
console.log("主题改变:", e);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chat:title-updated", { detail: e }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 供全局调用
|
||||
window.initChat = initChat;
|
||||
@@ -0,0 +1,47 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '仪表盘')
|
||||
|
||||
@section('content')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<h3 class="text-gray-500 text-sm font-medium mb-1">总计注册用户数</h3>
|
||||
<p class="text-3xl font-bold text-gray-800">{{ $stats['total_users'] }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<h3 class="text-gray-500 text-sm font-medium mb-1">总计聊天频道数</h3>
|
||||
<p class="text-3xl font-bold text-gray-800">{{ $stats['total_rooms'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h2 class="text-lg font-bold text-gray-800">系统信息摘要</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<ul class="space-y-3">
|
||||
<li class="flex items-center font-mono text-sm">
|
||||
<span class="w-32 text-gray-500 inline-block font-sans">Laravel 版本:</span>
|
||||
<span class="text-indigo-600">{{ app()->version() }}</span>
|
||||
</li>
|
||||
<li class="flex items-center font-mono text-sm">
|
||||
<span class="w-32 text-gray-500 inline-block font-sans">PHP 版本:</span>
|
||||
<span class="text-indigo-600">{{ PHP_VERSION }}</span>
|
||||
</li>
|
||||
<li class="flex items-center text-sm font-mono mt-4 pt-4 border-t">
|
||||
<span class="mr-4 text-gray-500 inline-block font-sans items-center flex">队列监控面板</span>
|
||||
<!-- Laravel Horizon 的默认路由前缀由开发者确认或自己改。这里默认是 /horizon -->
|
||||
<a href="{{ url('/horizon') }}" target="_blank"
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline flex items-center">
|
||||
<span>打开 Horizon 控制台</span>
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,83 @@
|
||||
<!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>
|
||||
|
||||
<body class="bg-gray-100 flex h-screen text-gray-800">
|
||||
<!-- 左侧侧边栏 -->
|
||||
<aside class="w-64 bg-slate-900 text-white flex flex-col">
|
||||
<div class="p-6 text-center border-b border-white/10">
|
||||
<h2 class="text-2xl font-extrabold tracking-widest uppercase">Admin</h2>
|
||||
<p class="text-xs text-slate-400 mt-2">飘落流星 控制台</p>
|
||||
</div>
|
||||
<nav class="flex-1 px-4 py-6 space-y-2 overflow-y-auto">
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.dashboard') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
📊 仪表盘
|
||||
</a>
|
||||
<a href="{{ route('admin.system.edit') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.system.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
⚙️ 系统参数参数
|
||||
</a>
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
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>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-white/10">
|
||||
<a href="{{ route('rooms.index') }}"
|
||||
class="block w-full text-center px-4 py-2 bg-slate-800 hover:bg-slate-700 rounded transition text-sm">
|
||||
返回前台大厅
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧主体内容 -->
|
||||
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
|
||||
<!-- 顶栏 -->
|
||||
<header class="bg-white shadow relative z-20 flex items-center justify-between px-6 py-4">
|
||||
<h1 class="text-xl font-bold text-gray-700">@yield('title', '控制台')</h1>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm font-medium">当前操作人: <span
|
||||
class="text-indigo-600">{{ Auth::user()->username }}</span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容滚动区 -->
|
||||
<div class="flex-1 overflow-y-auto p-6 relative z-10">
|
||||
@if (session('success'))
|
||||
<div class="mb-6 bg-emerald-100 border-l-4 border-emerald-500 text-emerald-700 p-4 rounded shadow-sm">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="mb-6 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-sm">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 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
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
@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
|
||||
@@ -0,0 +1,44 @@
|
||||
@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">保存后会同步更新 Redis 缓存,前台实时生效。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form action="{{ route('admin.system.update') }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="space-y-6 max-w-2xl">
|
||||
@foreach ($params as $alias => $body)
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">
|
||||
{{ $descriptions[$alias] ?? $alias }}
|
||||
<span class="text-gray-400 font-normal ml-2">[{{ $alias }}]</span>
|
||||
</label>
|
||||
@if (strlen($body) > 50 || str_contains($body, "\n") || str_contains($body, '<'))
|
||||
<textarea name="{{ $alias }}" rows="4"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border whitespace-pre-wrap">{{ $body }}</textarea>
|
||||
@else
|
||||
<input type="text" name="{{ $alias }}" value="{{ $body }}"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t flex space-x-3">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 shadow-sm transition">保存并发布</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,138 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '用户检索与管理')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6" x-data="{ showEditModal: false, editingUser: {} }">
|
||||
<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="搜索用户名..."
|
||||
class="px-3 py-1.5 border border-gray-300 rounded shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white px-4 py-1.5 rounded hover:bg-indigo-700 font-bold shadow-sm transition">搜索</button>
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="px-4 py-1.5 bg-white border border-gray-300 rounded text-gray-700 hover:bg-gray-50">重置</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr
|
||||
class="bg-gray-50 border-b border-gray-100 text-xs text-gray-500 uppercase font-bold tracking-wider">
|
||||
<th class="p-4">ID</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>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach ($users as $user)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="p-4 font-mono text-xs text-gray-500">{{ $user->id }}</td>
|
||||
<td class="p-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="/images/headface/{{ $user->headface ?? '01.gif' }}"
|
||||
class="w-8 h-8 rounded border object-cover">
|
||||
<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">
|
||||
<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' }}">
|
||||
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-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"
|
||||
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>
|
||||
|
||||
<form action="{{ route('admin.users.destroy', $user->id) }}" method="POST" class="inline"
|
||||
onsubmit="return confirm('危险:确定彻底物理清除用户 [{{ $user->username }}] 吗?数据不可恢复!')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit"
|
||||
class="text-xs bg-red-50 text-red-600 font-bold px-3 py-1.5 rounded hover:bg-red-600 hover:text-white transition cursor-pointer">
|
||||
强杀
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页链接 -->
|
||||
@if ($users->hasPages())
|
||||
<div class="p-4 border-t border-gray-100">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 弹出的修改框 -->
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
@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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="mb-6">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,488 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ $room->name ?? '聊天室' }} - 飘落流星</title>
|
||||
<!-- 引入全局 CSRF Token -->
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<!-- 传递必要的上下文数据给 chat.js 模块使用 -->
|
||||
<script>
|
||||
window.chatContext = {
|
||||
roomId: {{ $room->id }},
|
||||
username: "{{ $user->username }}",
|
||||
userLevel: {{ $user->user_level }},
|
||||
sendUrl: "{{ route('chat.send', $room->id) }}",
|
||||
leaveUrl: "{{ route('chat.leave', $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>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 flex h-screen overflow-hidden text-sm">
|
||||
|
||||
<!-- 左侧/中间部分:主聊天区域与控制台 -->
|
||||
<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>
|
||||
|
||||
<!-- 聊天记录展示区 -->
|
||||
<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>
|
||||
|
||||
<!-- 底部发言控制区 -->
|
||||
<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>
|
||||
</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>
|
||||
</form>
|
||||
</footer>
|
||||
</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 作用域外置挂载) -->
|
||||
<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));
|
||||
this.userInfo = await res.json();
|
||||
this.showUserModal = true;
|
||||
this.isMuting = false;
|
||||
} catch (e) {
|
||||
alert('获取资料失败');
|
||||
}
|
||||
},
|
||||
|
||||
// 执行踢出
|
||||
async kickUser() {
|
||||
if (!confirm('确定要将 ' + this.userInfo.username + ' 踢出房间吗?')) return;
|
||||
try {
|
||||
const res = await fetch('/user/' + encodeURIComponent(this.userInfo.username) + '/kick', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'), 'Content-Type'
|
||||
: 'application/json' , 'Accept' : 'application/json' }, body: JSON.stringify({ room_id:
|
||||
window.chatContext.roomId }) }); const data=await res.json(); if(data.status === 'success') {
|
||||
this.showUserModal=false; } else { alert('操作失败:' + data.message); } } catch (e) { alert('网络异常'); } }, // 执行禁言
|
||||
async muteUser() { try { const res=await fetch('/user/' + encodeURIComponent(this.userInfo.username) + '/mute' ,
|
||||
{ method: 'POST' , headers: { 'X-CSRF-TOKEN' :
|
||||
document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'), 'Content-Type' : 'application/json'
|
||||
, 'Accept' : 'application/json' }, body: JSON.stringify({ room_id: window.chatContext.roomId, duration:
|
||||
this.muteDuration }) }); const data=await res.json(); if(data.status === 'success') { alert(data.message);
|
||||
this.showUserModal=false; } else { alert('操作失败:' + data.message); } } catch (e) { alert('网络异常'); } } }">
|
||||
<!-- 用户名片弹窗 -->
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div> @endif
|
||||
</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>
|
||||
飞鸽传书 (发私信)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 核心页面交互逻辑,连接 chat.js 抛出的自定义事件
|
||||
|
||||
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');
|
||||
|
||||
let onlineUsers = {}; // 用于本地维护在线名单
|
||||
|
||||
// 辅助:滚动到底部
|
||||
function scrollToBottom() {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// 辅助:渲染在线人员列表
|
||||
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,244 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '星光留言板')
|
||||
|
||||
@section('content')
|
||||
<div class="h-screen w-full flex flex-col bg-gray-50 overflow-hidden font-sans" x-data="{ showWriteForm: false, towho: '{{ $defaultTo }}' }">
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<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>
|
||||
|
||||
<!-- 右侧:写留言按钮 -->
|
||||
<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">
|
||||
<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>
|
||||
</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 ($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 = $msg->towho === Auth::user()->username;
|
||||
$isFromMe = $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::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 ($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>
|
||||
|
||||
<!-- 移动端底部分类栏 -->
|
||||
<div class="md:hidden bg-white border-t border-gray-200 flex justify-around p-2 shrink-0">
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'public']) }}"
|
||||
class="flex flex-col items-center {{ $tab === 'public' ? 'text-indigo-600' : 'text-gray-500' }}">
|
||||
<span class="text-xl">🌍</span>
|
||||
<span class="text-xs mt-1">公共墙</span>
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'inbox']) }}"
|
||||
class="flex flex-col items-center {{ $tab === 'inbox' ? 'text-indigo-600' : 'text-gray-500' }}">
|
||||
<span class="text-xl">📥</span>
|
||||
<span class="text-xs mt-1">收件箱</span>
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'outbox']) }}"
|
||||
class="flex flex-col items-center {{ $tab === 'outbox' ? 'text-indigo-600' : 'text-gray-500' }}">
|
||||
<span class="text-xl">📤</span>
|
||||
<span class="text-xs mt-1">发件箱</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,130 @@
|
||||
<!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>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
</head>
|
||||
<body class="bg-gray-100 h-screen flex items-center justify-center">
|
||||
|
||||
<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>
|
||||
|
||||
<p class="text-sm text-gray-500 text-center mb-6">
|
||||
{{ \App\Models\SysParam::where('alias', 'sys_notice')->value('body') ?? '欢迎您的加入' }}
|
||||
</p>
|
||||
|
||||
<!-- 登录提示区 -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
// 刷新验证码
|
||||
function refreshCaptcha() {
|
||||
document.getElementById('captcha-img').src = '{{ captcha_src() }}' + Math.random();
|
||||
}
|
||||
|
||||
// 提交登录表单
|
||||
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');
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
fetch('{{ route("login.post") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json().then(data => ({ status: response.status, body: data })))
|
||||
.then(result => {
|
||||
if (result.status === 200 && result.body.status === 'success') {
|
||||
// 登录成功,显示成功并跳转
|
||||
showAlert(result.body.message, 'success');
|
||||
setTimeout(() => {
|
||||
// TODO: 之后重定向到真实的聊天室页面 /chat
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
// 验证失败或密码错误
|
||||
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 = '进入聊天室';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showAlert('网络或服务器错误,请稍后再试。', 'error');
|
||||
refreshCaptcha();
|
||||
btn.disabled = false;
|
||||
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');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,131 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '风云排行榜')
|
||||
|
||||
@section('content')
|
||||
<div class="h-screen w-full flex flex-col bg-gray-100 overflow-hidden font-sans">
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<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>
|
||||
|
||||
<!-- 右侧:当前用户状态 -->
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 说明条 -->
|
||||
<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">
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,57 @@
|
||||
<ul class="divide-y divide-gray-100">
|
||||
@forelse($users as $index => $user)
|
||||
@php
|
||||
// 前三名前景色
|
||||
$rankBg = 'bg-gray-100 text-gray-500';
|
||||
$rowBg = 'hover:bg-gray-50';
|
||||
if ($index === 0) {
|
||||
$rankBg = 'bg-yellow-400 text-yellow-900 shadow-md transform scale-110';
|
||||
$rowBg = 'bg-yellow-50 hover:bg-yellow-100 border-l-4 border-yellow-400';
|
||||
} elseif ($index === 1) {
|
||||
$rankBg = 'bg-gray-300 text-gray-800 shadow-sm transform scale-105';
|
||||
$rowBg = 'bg-gray-50 hover:bg-gray-100 border-l-4 border-gray-300';
|
||||
} elseif ($index === 2) {
|
||||
$rankBg = 'bg-orange-300 text-orange-900 shadow-sm';
|
||||
$rowBg = 'bg-orange-50 hover:bg-orange-100 border-l-4 border-orange-300';
|
||||
}
|
||||
@endphp
|
||||
<li class="p-3 flex items-center justify-between transition-colors duration-150 {{ $rowBg }}">
|
||||
<!-- 左侧:名次与头像/名字 -->
|
||||
<div class="flex items-center space-x-3 overflow-hidden">
|
||||
<div
|
||||
class="w-6 h-6 shrink-0 {{ $rankBg }} rounded-full flex items-center justify-center font-bold text-xs">
|
||||
{{ $index + 1 }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 truncate">
|
||||
<img class="w-8 h-8 rounded border object-cover shrink-0"
|
||||
src="/images/headface/{{ $user->headface ?? '01.gif' }}" alt="">
|
||||
<div class="flex flex-col truncate">
|
||||
<span class="text-sm font-bold text-gray-800 truncate" title="{{ $user->username }}">
|
||||
{{ $user->username }}
|
||||
@if ($user->sex == '女')
|
||||
<span class="text-pink-500 text-xs ml-0.5">♀</span>
|
||||
@elseif($user->sex == '男')
|
||||
<span class="text-blue-500 text-xs ml-0.5">♂</span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="text-[10px] text-gray-500 truncate"
|
||||
title="{{ $user->sign }}">{{ $user->sign ?: '这家伙很懒,什么也没留下' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:数值 -->
|
||||
<div class="flex flex-col items-end shrink-0 ml-2">
|
||||
<span class="text-sm font-black {{ $index < 3 ? $color : 'text-gray-600' }}">
|
||||
{{ number_format($user->$valueField) }}
|
||||
<span
|
||||
class="text-[10px] font-normal {{ $index < 3 ? $color : 'text-gray-400' }} ml-0.5">{{ $unit }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="p-8 text-center text-sm text-gray-400 font-bold">
|
||||
暂无数据登榜
|
||||
</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
@@ -0,0 +1,434 @@
|
||||
<!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>
|
||||
<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>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 min-h-screen text-gray-800" 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">
|
||||
<ul class="list-disc list-inside text-sm">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<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>
|
||||
</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
|
||||
class="bg-white rounded-xl shadow-sm hover:shadow-lg transition-shadow border border-gray-100 overflow-hidden flex flex-col group {{ $room->is_system ? 'ring-2 ring-indigo-500/20' : '' }}">
|
||||
|
||||
{{-- 卡片头部 --}}
|
||||
<div class="p-5 flex-1 relative">
|
||||
@if ($room->is_system)
|
||||
<span
|
||||
class="absolute top-4 right-4 bg-indigo-100 text-indigo-700 text-xs font-bold px-2 py-1 rounded-sm">官方驻地</span>
|
||||
@endif
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-2 truncate" title="{{ $room->name }}">
|
||||
{{ $room->name }}</h3>
|
||||
<p class="text-sm text-gray-500 line-clamp-2 h-10 mb-4">
|
||||
{{ $room->description ?: '房主很懒,什么都没写...' }}</p>
|
||||
<div class="flex items-center text-xs text-gray-400 font-medium">
|
||||
<span>房主:<span
|
||||
class="{{ $room->master == Auth::user()->username ? 'text-indigo-600 font-bold' : 'text-gray-600' }}">{{ $room->master }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 底部操作区 --}}
|
||||
<div class="bg-gray-50 p-4 border-t border-gray-100 flex items-center justify-between">
|
||||
|
||||
{{-- 管理按钮组(仅房主或超管可见) --}}
|
||||
<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">
|
||||
管理
|
||||
</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"
|
||||
onsubmit="return confirm('警告:确实要彻底解散「{{ $room->name }}」吗?此操作不可逆!');">
|
||||
@csrf @method('delete')
|
||||
<button type="submit"
|
||||
class="text-xs text-red-600 hover:text-red-800 font-semibold px-2 py-1 rounded hover:bg-red-50 transition">解散</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 进入按钮 --}}
|
||||
<a href="{{ route('chat.room', $room->id) }}"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full py-20 text-center text-gray-400">
|
||||
<p class="text-xl font-bold mb-2">大厅空空如也</p>
|
||||
<p>目前还没有人创建任何房间。</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 新建房间 Modal (通过 Alpine.js 开关) -->
|
||||
<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"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-gray-50 border-b px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg text-gray-800">新建私人聊天频道</h3>
|
||||
<button @click="showCreateModal = false"
|
||||
class="text-gray-400 hover:text-gray-600 font-bold text-xl">×</button>
|
||||
</div>
|
||||
<form action="{{ route('rooms.store') }}" method="POST" class="p-6">
|
||||
@csrf
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">频道名称 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input type="text" name="name" required maxlength="50" placeholder="例如:技术交流水群"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">频道主题描述</label>
|
||||
<textarea name="description" rows="3" maxlength="255" placeholder="一句话介绍这里的规矩..."
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border resize-none"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showCreateModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium">取消</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-emerald-600 text-white rounded-md font-bold hover:bg-emerald-700 shadow-sm">立即创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改管理 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"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-blue-50 border-b border-blue-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg text-blue-900" x-text="'管理频道: ' + currentRoom?.name"></h3>
|
||||
<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">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">频道新名称 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input type="text" name="name" :value="currentRoom?.name" required maxlength="50"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">更新主题描述</label>
|
||||
<textarea name="description" x-text="currentRoom?.description" rows="3" maxlength="255"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2.5 bg-gray-50 border resize-none"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showEditModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium">关闭</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-md font-bold hover:bg-blue-700 shadow-sm">保存并广播</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 转让房主 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"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-amber-50 border-b border-amber-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg text-amber-900" x-text="'转让授权: ' + currentRoom?.name"></h3>
|
||||
<button @click="showTransferModal = false"
|
||||
class="text-amber-400 hover:text-amber-600 font-bold text-xl">×</button>
|
||||
</div>
|
||||
<form :action="'{{ url('rooms') }}/' + currentRoom?.id + '/transfer'" method="POST" class="p-6">
|
||||
@csrf
|
||||
<div class="bg-amber-100 text-amber-800 text-xs p-3 rounded mb-4">
|
||||
转让后您将立即失去对该房间的管理权限,请谨慎操作。
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">接收人昵称 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input type="text" name="target_username" required placeholder="请输入对方准确用户名"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-amber-500 focus:ring-amber-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showTransferModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium">取消</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-amber-600 text-white rounded-md font-bold hover:bg-amber-700 shadow-sm">确认转让</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人资料设置 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"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div
|
||||
class="bg-gradient-to-r from-indigo-500 to-purple-600 px-6 py-4 flex justify-between items-center text-white">
|
||||
<h3 class="font-bold text-lg">个人资料设置</h3>
|
||||
<button @click="showProfileModal = false"
|
||||
class="text-white/80 hover:text-white font-bold text-xl">×</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6" x-data="{
|
||||
profileData: {
|
||||
sex: '{{ Auth::user()->sex }}',
|
||||
sign: '{{ Auth::user()->sign }}',
|
||||
headface: '{{ Auth::user()->headface ?? '01.gif' }}'
|
||||
},
|
||||
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; } } }">
|
||||
<form @submit.prevent="saveProfile">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">性别</label>
|
||||
<select x-model="profileData.sex"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
<option value="男">男生</option>
|
||||
<option value="女">女生</option>
|
||||
<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">
|
||||
<div class="w-12 h-12 rounded bg-gray-200 shrink-0 overflow-hidden border">
|
||||
<img :src="'/images/headface/' + profileData.headface"
|
||||
@error="$el.style.display='none'" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<input type="text" x-model="profileData.headface" required
|
||||
class="flex-1 border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">个性签名</label>
|
||||
<textarea x-model="profileData.sign" rows="2" maxlength="255" placeholder="写点什么吧..."
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border resize-none"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-between items-center border-t pt-4">
|
||||
<button type="button" @click="showProfileModal = false; showPasswordModal = true"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800 font-bold hover:underline">修改安全密码</button>
|
||||
<div class="space-x-2">
|
||||
<button type="button" @click="showProfileModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium cursor-pointer">取消</button>
|
||||
<button type="submit" :disabled="isSaving"
|
||||
class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 shadow-sm disabled:opacity-50 cursor-pointer">保存资料</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码 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"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-gray-800 px-6 py-4 flex justify-between items-center text-white">
|
||||
<h3 class="font-bold text-lg">修改安全密码</h3>
|
||||
<button @click="showPasswordModal = false"
|
||||
class="text-gray-400 hover:text-white font-bold text-xl">×</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6" x-data="{
|
||||
pwdData: {
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
new_password_confirmation: ''
|
||||
},
|
||||
isSaving: false,
|
||||
|
||||
async savePassword() {
|
||||
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 || ' 请输入正确的旧密码')); } } 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>
|
||||
<input type="password" x-model="pwdData.old_password" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-gray-500 focus:ring-gray-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">新密码</label>
|
||||
<input type="password" x-model="pwdData.new_password" required placeholder="至少 6 位字母或数字"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-gray-500 focus:ring-gray-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">确认新密码</label>
|
||||
<input type="password" x-model="pwdData.new_password_confirmation" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-gray-500 focus:ring-gray-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 border-t pt-4">
|
||||
<button type="button" @click="showPasswordModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium cursor-pointer">取消</button>
|
||||
<button type="submit" :disabled="isSaving"
|
||||
class="px-6 py-2 bg-gray-800 text-white rounded-md font-bold hover:bg-gray-900 shadow-sm disabled:opacity-50 cursor-pointer">确定修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user