功能:字体颜色持久化、等级体系升级至99级、钓鱼小游戏、补充系统参数
- 字体颜色:s_color 改为 varchar,发消息时保存颜色,进入聊天室自动恢复 - 等级体系:maxlevel 15→99,superlevel 16→100,99级经验阶梯(幂次曲线) - 管理权限等级按比例调整:禁言50、踢人60、设公告60、封号80、封IP90 - 钓鱼小游戏:FishingController(抛竿扣金币+收竿随机结果+广播) - 补充6个缺失的 sysparam 参数 + 4个钓鱼参数 - 用户列表点击用户名后自动聚焦输入框 - Pint 格式化
97
app/Http/Controllers/Admin/AutoactController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:自动事件管理控制器
|
||||
* 管理员可在后台增删改随机事件(好运/坏运/经验/金币奖惩等)
|
||||
* 复刻原版 ASP 聊天室的 autoact 管理功能
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Autoact;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AutoactController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示所有自动事件列表
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$events = Autoact::orderByDesc('id')->get();
|
||||
|
||||
return view('admin.autoact.index', compact('events'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新事件
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'text_body' => 'required|string|max:500',
|
||||
'event_type' => 'required|in:good,bad,neutral',
|
||||
'exp_change' => 'required|integer',
|
||||
'jjb_change' => 'required|integer',
|
||||
]);
|
||||
|
||||
$data['enabled'] = true;
|
||||
|
||||
Autoact::create($data);
|
||||
|
||||
return redirect()->route('admin.autoact.index')->with('success', '事件添加成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新事件
|
||||
*/
|
||||
public function update(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
$event = Autoact::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'text_body' => 'required|string|max:500',
|
||||
'event_type' => 'required|in:good,bad,neutral',
|
||||
'exp_change' => 'required|integer',
|
||||
'jjb_change' => 'required|integer',
|
||||
]);
|
||||
|
||||
$event->update($data);
|
||||
|
||||
return redirect()->route('admin.autoact.index')->with('success', '事件修改成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换事件启用/禁用状态
|
||||
*/
|
||||
public function toggle(int $id): JsonResponse
|
||||
{
|
||||
$event = Autoact::findOrFail($id);
|
||||
$event->enabled = ! $event->enabled;
|
||||
$event->save();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'enabled' => $event->enabled,
|
||||
'message' => $event->enabled ? '已启用' : '已禁用',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除事件
|
||||
*/
|
||||
public function destroy(int $id): RedirectResponse
|
||||
{
|
||||
Autoact::findOrFail($id)->delete();
|
||||
|
||||
return redirect()->route('admin.autoact.index')->with('success', '事件已删除!');
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/Admin/RoomManagerController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台房间管理控制器
|
||||
* 管理员可查看、编辑房间信息(名称、介绍、公告等)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Room;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RoomManagerController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示所有房间列表
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$rooms = Room::orderBy('id')->get();
|
||||
|
||||
return view('admin.rooms.index', compact('rooms'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新房间信息
|
||||
*/
|
||||
public function update(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
$room = Room::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'room_name' => 'required|string|max:100',
|
||||
'room_des' => 'nullable|string|max:500',
|
||||
'announcement' => 'nullable|string|max:500',
|
||||
'room_owner' => 'nullable|string|max:50',
|
||||
'permit_level' => 'required|integer|min:0|max:15',
|
||||
'door_open' => 'required|boolean',
|
||||
]);
|
||||
|
||||
$room->update($data);
|
||||
|
||||
return redirect()->route('admin.rooms.index')->with('success', "房间 [{$room->room_name}] 信息已更新!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除房间(非系统房间)
|
||||
*/
|
||||
public function destroy(int $id): RedirectResponse
|
||||
{
|
||||
$room = Room::findOrFail($id);
|
||||
|
||||
if ($room->room_keep) {
|
||||
return redirect()->route('admin.rooms.index')->with('error', '系统房间不允许删除!');
|
||||
}
|
||||
|
||||
$room->delete();
|
||||
|
||||
return redirect()->route('admin.rooms.index')->with('success', "房间 [{$room->room_name}] 已删除!");
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台 SQL 探针
|
||||
* (替代原版 SQL.ASP,严格限制为只读模式)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SqlController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示 SQL 执行沙盒界面
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.sql.index', ['results' => null, 'query' => '', 'columns' => []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 极度受限地执行 SQL (仅限 SELECT)
|
||||
*/
|
||||
public function execute(Request $request): View
|
||||
{
|
||||
$request->validate([
|
||||
'query' => 'required|string|min:6',
|
||||
]);
|
||||
|
||||
$sql = trim($request->input('query'));
|
||||
|
||||
// 安全拦截:绝不允许含有 update/delete/insert/truncate/drop 等破坏性指令
|
||||
// 我们只允许查询,所以要求必须以 SELECT 起手,或者 EXPLAIN/SHOW
|
||||
if (! preg_match('/^(SELECT|EXPLAIN|SHOW|DESCRIBE)\s/i', $sql)) {
|
||||
return view('admin.sql.index', [
|
||||
'results' => null,
|
||||
'columns' => [],
|
||||
'query' => $sql,
|
||||
'error' => '安全保护触发:本探针只允许执行 SELECT / SHOW 等只读查询!',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$results = DB::select($sql);
|
||||
|
||||
// 提取表头
|
||||
$columns = [];
|
||||
if (! empty($results)) {
|
||||
$firstRow = (array) $results[0];
|
||||
$columns = array_keys($firstRow);
|
||||
}
|
||||
|
||||
return view('admin.sql.index', [
|
||||
'results' => $results,
|
||||
'columns' => $columns,
|
||||
'query' => $sql,
|
||||
'error' => null,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return view('admin.sql.index', [
|
||||
'results' => null,
|
||||
'columns' => [],
|
||||
'query' => $sql,
|
||||
'error' => 'SQL 执行发生异常: '.$e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,9 @@ class SystemController extends Controller
|
||||
|
||||
// 写入 Cache 保证极速读取
|
||||
$this->chatState->setSysParam($alias, $body);
|
||||
|
||||
// 同时清除 Sysparam 模型的内部缓存
|
||||
SysParam::clearCache($alias);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.system.edit')->with('success', '系统参数已成功更新并生效!');
|
||||
|
||||
@@ -52,17 +52,23 @@ class UserManagerController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '权限不足:您无法修改同级或高级管理人员资料。'], 403);
|
||||
}
|
||||
|
||||
// 管理员级别 = 最高等级 + 1,后台编辑最高可设到管理员级别
|
||||
$adminLevel = (int) \App\Models\Sysparam::getValue('maxlevel', '15') + 1;
|
||||
|
||||
$validated = $request->validate([
|
||||
'sex' => 'sometimes|in:男,女,保密',
|
||||
'user_level' => 'sometimes|integer|min:0',
|
||||
'sex' => 'sometimes|integer|in:0,1,2',
|
||||
'user_level' => "sometimes|integer|min:0|max:{$adminLevel}",
|
||||
'exp_num' => 'sometimes|integer|min:0',
|
||||
'jjb' => 'sometimes|integer|min:0',
|
||||
'meili' => 'sometimes|integer|min:0',
|
||||
'qianming' => 'sometimes|nullable|string|max:255',
|
||||
'headface' => 'sometimes|string|max:50',
|
||||
'sign' => 'sometimes|string|max:255',
|
||||
'password' => 'nullable|string|min:6',
|
||||
]);
|
||||
|
||||
// 如果传了且没超权,直接赋予
|
||||
if (isset($validated['user_level'])) {
|
||||
// 不能把自己或别人提权到超过自己的等级
|
||||
// 不能把别人提权到超过自己的等级
|
||||
if ($validated['user_level'] > $currentUser->user_level && $currentUser->id !== $targetUser->id) {
|
||||
return response()->json(['status' => 'error', 'message' => '您不能将别人提升至超过您的等级!'], 403);
|
||||
}
|
||||
@@ -72,12 +78,21 @@ class UserManagerController extends Controller
|
||||
if (isset($validated['sex'])) {
|
||||
$targetUser->sex = $validated['sex'];
|
||||
}
|
||||
if (isset($validated['exp_num'])) {
|
||||
$targetUser->exp_num = $validated['exp_num'];
|
||||
}
|
||||
if (isset($validated['jjb'])) {
|
||||
$targetUser->jjb = $validated['jjb'];
|
||||
}
|
||||
if (isset($validated['meili'])) {
|
||||
$targetUser->meili = $validated['meili'];
|
||||
}
|
||||
if (array_key_exists('qianming', $validated)) {
|
||||
$targetUser->qianming = $validated['qianming'];
|
||||
}
|
||||
if (isset($validated['headface'])) {
|
||||
$targetUser->headface = $validated['headface'];
|
||||
}
|
||||
if (isset($validated['sign'])) {
|
||||
$targetUser->sign = $validated['sign'];
|
||||
}
|
||||
|
||||
if (! empty($validated['password'])) {
|
||||
$targetUser->password = Hash::make($validated['password']);
|
||||
|
||||
@@ -70,7 +70,7 @@ class AuthController extends Controller
|
||||
'last_ip' => $ip,
|
||||
'user_level' => 1, // 默认普通用户等级
|
||||
'sex' => 0, // 默认性别: 0保密 1男 2女
|
||||
// 如果原表里还有其他必填字段,在这里初始化默认值
|
||||
'usersf' => '1.GIF', // 默认头像
|
||||
]);
|
||||
|
||||
$this->performLogin($newUser, $ip);
|
||||
|
||||
@@ -16,12 +16,15 @@ use App\Events\UserJoined;
|
||||
use App\Events\UserLeft;
|
||||
use App\Http\Requests\SendMessageRequest;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\Autoact;
|
||||
use App\Models\Room;
|
||||
use App\Models\Sysparam;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\MessageFilterService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ChatController extends Controller
|
||||
@@ -42,6 +45,9 @@ class ChatController extends Controller
|
||||
$room = Room::findOrFail($id);
|
||||
$user = Auth::user();
|
||||
|
||||
// 房间人气 +1(每次访问递增,复刻原版人气计数)
|
||||
$room->increment('visit_num');
|
||||
|
||||
// 1. 将当前用户加入到 Redis 房间在线列表
|
||||
$this->chatState->userJoin($id, $user->username, [
|
||||
'level' => $user->user_level,
|
||||
@@ -76,6 +82,18 @@ class ChatController extends Controller
|
||||
$data = $request->validated();
|
||||
$user = Auth::user();
|
||||
|
||||
// 0. 检查用户是否被禁言(Redis TTL 自动过期)
|
||||
$muteKey = "mute:{$id}:{$user->username}";
|
||||
if (Redis::exists($muteKey)) {
|
||||
$ttl = Redis::ttl($muteKey);
|
||||
$minutes = ceil($ttl / 60);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "您正在禁言中,还需等待约 {$minutes} 分钟。",
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 1. 过滤净化消息体
|
||||
$pureContent = $this->filter->filter($data['content'] ?? '');
|
||||
if (empty($pureContent)) {
|
||||
@@ -104,6 +122,13 @@ class ChatController extends Controller
|
||||
// 5. 丢进异步列队,慢慢持久化到 MySQL,保护数据库连接池
|
||||
SaveMessageJob::dispatch($messageData);
|
||||
|
||||
// 6. 如果用户更换了字体颜色,顺便保存到 s_color 字段,下次进入时恢复
|
||||
$chosenColor = $data['font_color'] ?? '';
|
||||
if ($chosenColor && $chosenColor !== ($user->s_color ?? '')) {
|
||||
$user->s_color = $chosenColor;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
@@ -120,19 +145,22 @@ class ChatController extends Controller
|
||||
return response()->json(['status' => 'error'], 401);
|
||||
}
|
||||
|
||||
// 1. 每次心跳 +1 点经验
|
||||
$user->exp_num += 1;
|
||||
|
||||
// 2. 检查等级计算:设定简单粗暴的平滑算式:需要经验=等级*等级*10
|
||||
// 例如:0级->0点;1级->10点;2级->40点;3级->90点;10级->1000点
|
||||
$currentLevel = $user->user_level;
|
||||
$requiredExpForNextLevel = ($currentLevel) * ($currentLevel) * 10;
|
||||
// 1. 每次心跳增加经验(可在 sysparam 后台配置)
|
||||
$expGain = (int) Sysparam::getValue('exp_per_heartbeat', '1');
|
||||
$user->exp_num += $expGain;
|
||||
|
||||
// 2. 使用 sysparam 表中可配置的等级-经验阈值计算等级
|
||||
// 管理员(superlevel 及以上)不参与自动升降级,等级由后台手动设置
|
||||
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
||||
$oldLevel = $user->user_level;
|
||||
$leveledUp = false;
|
||||
|
||||
if ($user->exp_num >= $requiredExpForNextLevel) {
|
||||
$user->user_level += 1;
|
||||
$leveledUp = true;
|
||||
if ($oldLevel < $superLevel) {
|
||||
$newLevel = Sysparam::calculateLevel($user->exp_num);
|
||||
if ($newLevel !== $oldLevel && $newLevel < $superLevel) {
|
||||
$user->user_level = $newLevel;
|
||||
$leveledUp = ($newLevel > $oldLevel);
|
||||
}
|
||||
}
|
||||
|
||||
$user->save(); // 存点入库
|
||||
@@ -167,12 +195,61 @@ class ChatController extends Controller
|
||||
SaveMessageJob::dispatch($sysMsg);
|
||||
}
|
||||
|
||||
// 5. 随机事件触发(复刻原版 autoact 系统,概率可在后台配置)
|
||||
$autoEvent = null;
|
||||
$eventChance = (int) Sysparam::getValue('auto_event_chance', '10');
|
||||
if ($eventChance > 0 && rand(1, 100) <= $eventChance) {
|
||||
$autoEvent = Autoact::randomEvent();
|
||||
if ($autoEvent) {
|
||||
// 应用经验/金币变化(不低于 0)
|
||||
if ($autoEvent->exp_change !== 0) {
|
||||
$user->exp_num = max(0, $user->exp_num + $autoEvent->exp_change);
|
||||
}
|
||||
if ($autoEvent->jjb_change !== 0) {
|
||||
$user->jjb = max(0, ($user->jjb ?? 0) + $autoEvent->jjb_change);
|
||||
}
|
||||
$user->save();
|
||||
|
||||
// 重新计算等级(经验可能因事件而变化,但管理员不参与自动升降级)
|
||||
if ($user->user_level < $superLevel) {
|
||||
$recalcLevel = Sysparam::calculateLevel($user->exp_num);
|
||||
if ($recalcLevel !== $user->user_level && $recalcLevel < $superLevel) {
|
||||
$user->user_level = $recalcLevel;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
// 广播随机事件消息到聊天室
|
||||
$eventMsg = [
|
||||
'id' => $this->chatState->nextMessageId($id),
|
||||
'room_id' => $id,
|
||||
'from_user' => '星海小博士',
|
||||
'to_user' => '大家',
|
||||
'content' => $autoEvent->renderText($user->username),
|
||||
'is_secret' => false,
|
||||
'font_color' => match ($autoEvent->event_type) {
|
||||
'good' => '#16a34a', // 绿色(好运)
|
||||
'bad' => '#dc2626', // 红色(坏运)
|
||||
default => '#7c3aed', // 紫色(中性)
|
||||
},
|
||||
'action' => '',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$this->chatState->pushMessage($id, $eventMsg);
|
||||
broadcast(new MessageSent($id, $eventMsg));
|
||||
SaveMessageJob::dispatch($eventMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'exp_num' => $user->exp_num,
|
||||
'user_level' => $user->user_level,
|
||||
'leveled_up' => $leveledUp,
|
||||
'is_max_level' => $user->user_level >= $superLevel,
|
||||
'auto_event' => $autoEvent ? $autoEvent->renderText($user->username) : null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -197,4 +274,105 @@ class ChatController extends Controller
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用头像列表(返回 JSON)
|
||||
* 扫描 /public/images/headface/ 目录,返回所有可用头像文件名
|
||||
*/
|
||||
public function headfaceList(): JsonResponse
|
||||
{
|
||||
$dir = public_path('images/headface');
|
||||
$files = [];
|
||||
|
||||
if (is_dir($dir)) {
|
||||
$all = scandir($dir);
|
||||
foreach ($all as $file) {
|
||||
// 只包含图片文件
|
||||
if (preg_match('/\.(gif|jpg|jpeg|png|bmp)$/i', $file)) {
|
||||
$files[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自然排序(1, 2, 3... 10, 11...)
|
||||
natsort($files);
|
||||
|
||||
return response()->json(['headfaces' => array_values($files)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改头像(原版 fw.asp 功能)
|
||||
* 用户选择一个头像文件名,更新到 usersf 字段
|
||||
*/
|
||||
public function changeAvatar(Request $request): JsonResponse
|
||||
{
|
||||
$user = auth()->user();
|
||||
$headface = $request->input('headface', '');
|
||||
|
||||
if (empty($headface)) {
|
||||
return response()->json(['status' => 'error', 'message' => '请选择一个头像'], 422);
|
||||
}
|
||||
|
||||
// 验证文件确实存在
|
||||
if (! file_exists(public_path('images/headface/'.$headface))) {
|
||||
return response()->json(['status' => 'error', 'message' => '头像文件不存在'], 422);
|
||||
}
|
||||
|
||||
// 更新用户头像
|
||||
$user->usersf = $headface;
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => '头像修改成功!',
|
||||
'headface' => $headface,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间公告/祝福语(滚动显示在聊天室顶部)
|
||||
* 需要房间主人或等级达到 level_announcement 配置值
|
||||
*
|
||||
* @param int $id 房间ID
|
||||
*/
|
||||
public function setAnnouncement(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$room = Room::findOrFail($id);
|
||||
|
||||
// 权限检查:房间主人 或 等级 >= level_announcement
|
||||
$requiredLevel = (int) Sysparam::getValue('level_announcement', '10');
|
||||
if ($user->username !== $room->master && $user->user_level < $requiredLevel) {
|
||||
return response()->json(['status' => 'error', 'message' => '权限不足,无法修改公告'], 403);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'announcement' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
$room->announcement = $request->input('announcement');
|
||||
$room->save();
|
||||
|
||||
// 广播公告更新到所有在线用户
|
||||
$sysMsg = [
|
||||
'id' => $this->chatState->nextMessageId($id),
|
||||
'room_id' => $id,
|
||||
'from_user' => '系统公告',
|
||||
'to_user' => '大家',
|
||||
'content' => "📢 {$user->username} 更新了房间公告:{$room->announcement}",
|
||||
'is_secret' => false,
|
||||
'font_color' => '#cc0000',
|
||||
'action' => '',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$this->chatState->pushMessage($id, $sysMsg);
|
||||
broadcast(new MessageSent($id, $sysMsg));
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => '公告已更新!',
|
||||
'announcement' => $room->announcement,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
210
app/Http/Controllers/FishingController.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:钓鱼小游戏控制器
|
||||
* 复刻原版 ASP 聊天室 diaoyu/ 目录下的钓鱼功能
|
||||
* 简化掉鱼竿道具系统,用 Redis 控制冷却,随机奖惩经验/金币
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\Sysparam;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class FishingController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 抛竿 — 检查冷却和金币,扣除金币,返回随机等待时间
|
||||
*
|
||||
* @param int $id 房间ID
|
||||
*/
|
||||
public function cast(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user) {
|
||||
return response()->json(['status' => 'error', 'message' => '请先登录'], 401);
|
||||
}
|
||||
|
||||
// 1. 检查冷却时间(Redis TTL)
|
||||
$cooldownKey = "fishing:cd:{$user->id}";
|
||||
if (Redis::exists($cooldownKey)) {
|
||||
$ttl = Redis::ttl($cooldownKey);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "钓鱼冷却中,还需等待 {$ttl} 秒。",
|
||||
'cooldown' => $ttl,
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 2. 检查金币是否足够
|
||||
$cost = (int) Sysparam::getValue('fishing_cost', '5');
|
||||
if (($user->jjb ?? 0) < $cost) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "金币不足!钓鱼需要 {$cost} 金币,您当前只有 {$user->jjb} 金币。",
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 3. 扣除金币
|
||||
$user->jjb = max(0, ($user->jjb ?? 0) - $cost);
|
||||
$user->save();
|
||||
|
||||
// 4. 设置"正在钓鱼"标记(防止重复抛竿,30秒后自动过期)
|
||||
Redis::setex("fishing:active:{$user->id}", 30, time());
|
||||
|
||||
// 5. 计算随机等待时间
|
||||
$waitMin = (int) Sysparam::getValue('fishing_wait_min', '8');
|
||||
$waitMax = (int) Sysparam::getValue('fishing_wait_max', '15');
|
||||
$waitTime = rand($waitMin, $waitMax);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => "已花费 {$cost} 金币,鱼竿已抛出!等待鱼儿上钩...",
|
||||
'wait_time' => $waitTime,
|
||||
'cost' => $cost,
|
||||
'jjb' => $user->jjb,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收竿 — 随机计算钓鱼结果,更新经验/金币,广播到聊天室
|
||||
*
|
||||
* @param int $id 房间ID
|
||||
*/
|
||||
public function reel(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (! $user) {
|
||||
return response()->json(['status' => 'error', 'message' => '请先登录'], 401);
|
||||
}
|
||||
|
||||
// 1. 检查是否有"正在钓鱼"标记
|
||||
$activeKey = "fishing:active:{$user->id}";
|
||||
if (! Redis::exists($activeKey)) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => '您还没有抛竿,或者鱼已经跑了!',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 清除钓鱼标记
|
||||
Redis::del($activeKey);
|
||||
|
||||
// 2. 设置冷却时间
|
||||
$cooldown = (int) Sysparam::getValue('fishing_cooldown', '300');
|
||||
Redis::setex("fishing:cd:{$user->id}", $cooldown, time());
|
||||
|
||||
// 3. 随机决定钓鱼结果
|
||||
$result = $this->randomFishResult();
|
||||
|
||||
// 4. 更新用户经验和金币(不低于 0)
|
||||
if ($result['exp'] !== 0) {
|
||||
$user->exp_num = max(0, ($user->exp_num ?? 0) + $result['exp']);
|
||||
}
|
||||
if ($result['jjb'] !== 0) {
|
||||
$user->jjb = max(0, ($user->jjb ?? 0) + $result['jjb']);
|
||||
}
|
||||
$user->save();
|
||||
|
||||
// 5. 广播钓鱼结果到聊天室
|
||||
$sysMsg = [
|
||||
'id' => $this->chatState->nextMessageId($id),
|
||||
'room_id' => $id,
|
||||
'from_user' => '钓鱼播报',
|
||||
'to_user' => '大家',
|
||||
'content' => "{$result['emoji']} {$user->username}{$result['message']}",
|
||||
'is_secret' => false,
|
||||
'font_color' => $result['exp'] >= 0 ? '#16a34a' : '#dc2626',
|
||||
'action' => '',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$this->chatState->pushMessage($id, $sysMsg);
|
||||
broadcast(new MessageSent($id, $sysMsg));
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'result' => $result,
|
||||
'exp_num' => $user->exp_num,
|
||||
'jjb' => $user->jjb,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机钓鱼结果(复刻原版概率分布)
|
||||
*
|
||||
* @return array{emoji: string, message: string, exp: int, jjb: int}
|
||||
*/
|
||||
private function randomFishResult(): array
|
||||
{
|
||||
$roll = rand(1, 100);
|
||||
|
||||
// 概率分布(总计 100%)
|
||||
// 1-15: 大鲨鱼 (+100exp, +20金)
|
||||
// 16-30: 娃娃鱼 (+0exp, +30金)
|
||||
// 31-50: 大草鱼 (+50exp)
|
||||
// 51-70: 小鲤鱼 (+50exp, +10金)
|
||||
// 71-85: 落水 (-50exp)
|
||||
// 86-95: 被打 (-20exp, -3金)
|
||||
// 96-100:大丰收 (+150exp, +50金)
|
||||
|
||||
return match (true) {
|
||||
$roll <= 15 => [
|
||||
'emoji' => '🦈',
|
||||
'message' => '钓到一条大鲨鱼!增加经验100、金币20',
|
||||
'exp' => 100,
|
||||
'jjb' => 20,
|
||||
],
|
||||
$roll <= 30 => [
|
||||
'emoji' => '🐟',
|
||||
'message' => '钓到一条娃娃鱼,到集市卖得30个金币',
|
||||
'exp' => 0,
|
||||
'jjb' => 30,
|
||||
],
|
||||
$roll <= 50 => [
|
||||
'emoji' => '🐠',
|
||||
'message' => '钓到一只大草鱼,吃下增加经验50',
|
||||
'exp' => 50,
|
||||
'jjb' => 0,
|
||||
],
|
||||
$roll <= 70 => [
|
||||
'emoji' => '🐡',
|
||||
'message' => '钓到一条小鲤鱼,增加经验50、金币10',
|
||||
'exp' => 50,
|
||||
'jjb' => 10,
|
||||
],
|
||||
$roll <= 85 => [
|
||||
'emoji' => '💧',
|
||||
'message' => '鱼没钓到,摔到河里经验减少50',
|
||||
'exp' => -50,
|
||||
'jjb' => 0,
|
||||
],
|
||||
$roll <= 95 => [
|
||||
'emoji' => '👊',
|
||||
'message' => '偷钓鱼塘被主人发现,一阵殴打!经验减少20、金币减少3',
|
||||
'exp' => -20,
|
||||
'jjb' => -3,
|
||||
],
|
||||
default => [
|
||||
'emoji' => '🎉',
|
||||
'message' => '运气爆棚!钓到大鲨鱼、大草鱼、小鲤鱼各一条!经验+150,金币+50!',
|
||||
'exp' => 150,
|
||||
'jjb' => 50,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class LeaderboardController extends Controller
|
||||
|
||||
// 1. 境界榜 (以 user_level 为尊)
|
||||
$topLevels = Cache::remember('leaderboard:top_levels', $ttl, function () {
|
||||
return User::select('id', 'username', 'headface', 'user_level', 'sex')
|
||||
return User::select('id', 'username', 'usersf', 'user_level', 'sex')
|
||||
->where('user_level', '>', 0)
|
||||
->orderByDesc('user_level')
|
||||
->orderBy('id')
|
||||
@@ -38,7 +38,7 @@ class LeaderboardController extends Controller
|
||||
|
||||
// 2. 修为榜 (以 exp_num 为尊)
|
||||
$topExp = Cache::remember('leaderboard:top_exp', $ttl, function () {
|
||||
return User::select('id', 'username', 'headface', 'exp_num', 'sex', 'user_level')
|
||||
return User::select('id', 'username', 'usersf', 'exp_num', 'sex', 'user_level')
|
||||
->where('exp_num', '>', 0)
|
||||
->orderByDesc('exp_num')
|
||||
->orderBy('id')
|
||||
@@ -48,7 +48,7 @@ class LeaderboardController extends Controller
|
||||
|
||||
// 3. 财富榜 (以 jjb-交友币 为尊)
|
||||
$topWealth = Cache::remember('leaderboard:top_wealth', $ttl, function () {
|
||||
return User::select('id', 'username', 'headface', 'jjb', 'sex', 'user_level')
|
||||
return User::select('id', 'username', 'usersf', 'jjb', 'sex', 'user_level')
|
||||
->where('jjb', '>', 0)
|
||||
->orderByDesc('jjb')
|
||||
->orderBy('id')
|
||||
@@ -58,7 +58,7 @@ class LeaderboardController extends Controller
|
||||
|
||||
// 4. 魅力榜 (以 meili 为尊)
|
||||
$topCharm = Cache::remember('leaderboard:top_charm', $ttl, function () {
|
||||
return User::select('id', 'username', 'headface', 'meili', 'sex', 'user_level')
|
||||
return User::select('id', 'username', 'usersf', 'meili', 'sex', 'user_level')
|
||||
->where('meili', '>', 0)
|
||||
->orderByDesc('meili')
|
||||
->orderBy('id')
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
|
||||
/**
|
||||
* 文件功能:用户中心与管理控制器
|
||||
* 接管原版 USERinfo.ASP, USERSET.ASP, chpasswd.asp, KILLUSER.ASP
|
||||
* 接管原版 USERinfo.ASP, USERSET.ASP, chpasswd.asp, KILLUSER.ASP, LOCKIP.ASP
|
||||
*
|
||||
* 权限等级通过 sysparam 表动态配置:
|
||||
* level_kick - 踢人所需等级
|
||||
* level_mute - 禁言所需等级
|
||||
* level_ban - 封号所需等级
|
||||
* level_banip - 封IP所需等级
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
@@ -16,11 +22,13 @@ use App\Events\UserMuted;
|
||||
use App\Http\Requests\ChangePasswordRequest;
|
||||
use App\Http\Requests\UpdateProfileRequest;
|
||||
use App\Models\Room;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
@@ -36,7 +44,11 @@ class UserController extends Controller
|
||||
'username' => $user->username,
|
||||
'sex' => $user->sex,
|
||||
'headface' => $user->headface,
|
||||
'usersf' => $user->usersf,
|
||||
'user_level' => $user->user_level,
|
||||
'exp_num' => $user->exp_num ?? 0,
|
||||
'jjb' => $user->jjb ?? 0,
|
||||
'qianming' => $user->qianming,
|
||||
'sign' => $user->sign ?? '这个人很懒,什么都没留下。',
|
||||
'created_at' => $user->created_at->format('Y-m-d'),
|
||||
]);
|
||||
@@ -85,7 +97,41 @@ class UserController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员/房主操作:踢出房间 (对应 KILLUSER.ASP)
|
||||
* 通用权限校验:检查操作者是否有权操作目标用户
|
||||
*
|
||||
* @param object $operator 操作者
|
||||
* @param string $targetUsername 目标用户名
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $levelKey sysparam中的等级键名(如 level_kick)
|
||||
* @param string $actionName 操作名称(用于错误提示)
|
||||
* @return array{room: Room, target: User}|JsonResponse
|
||||
*/
|
||||
private function checkPermission(object $operator, string $targetUsername, int $roomId, string $levelKey, string $actionName): array|JsonResponse
|
||||
{
|
||||
$room = Room::findOrFail($roomId);
|
||||
$requiredLevel = (int) Sysparam::getValue($levelKey, '15');
|
||||
|
||||
// 鉴权:操作者要是房间房主或达到所需等级
|
||||
if ($room->master !== $operator->username && $operator->user_level < $requiredLevel) {
|
||||
return response()->json(['status' => 'error', 'message' => "权限不足(需要{$requiredLevel}级),无法执行{$actionName}操作。"], 403);
|
||||
}
|
||||
|
||||
$targetUser = User::where('username', $targetUsername)->first();
|
||||
if (! $targetUser) {
|
||||
return response()->json(['status' => 'error', 'message' => '目标用户不存在。'], 404);
|
||||
}
|
||||
|
||||
// 防误伤:不能操作等级 >= 自己的人
|
||||
if ($targetUser->user_level >= $operator->user_level) {
|
||||
return response()->json(['status' => 'error', 'message' => "权限不足,无法对同级或高级用户执行{$actionName}。"], 403);
|
||||
}
|
||||
|
||||
return ['room' => $room, 'target' => $targetUser];
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢出房间 (对应 KILLUSER.ASP)
|
||||
* 所需等级由 sysparam level_kick 配置
|
||||
*/
|
||||
public function kick(Request $request, string $username): JsonResponse
|
||||
{
|
||||
@@ -96,54 +142,113 @@ class UserController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
||||
}
|
||||
|
||||
$room = Room::findOrFail($roomId);
|
||||
|
||||
// 鉴权:操作者要是房间房主或者系统超管
|
||||
if ($room->master !== $operator->username && $operator->user_level < 15) {
|
||||
return response()->json(['status' => 'error', 'message' => '权限不足,无法执行踢出操作。'], 403);
|
||||
$result = $this->checkPermission($operator, $username, $roomId, 'level_kick', '踢出');
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$targetUser = User::where('username', $username)->first();
|
||||
if (! $targetUser) {
|
||||
return response()->json(['status' => 'error', 'message' => '目标用户不存在。'], 404);
|
||||
}
|
||||
// 广播踢出事件
|
||||
broadcast(new UserKicked($roomId, $result['target']->username, "管理员 [{$operator->username}] 将 [{$result['target']->username}] 踢出了聊天室。"));
|
||||
|
||||
// 防误伤高管
|
||||
if ($targetUser->user_level >= 15 && $operator->user_level < 15) {
|
||||
return response()->json(['status' => 'error', 'message' => '权限不足,无法踢出同级或高级管理人员。'], 403);
|
||||
}
|
||||
|
||||
// 核心动作:向频道内所有人发送包含“某某踢出某某”的事件
|
||||
broadcast(new UserKicked($roomId, $targetUser->username, "管理员 [{$operator->username}] 将 [{$targetUser->username}] 踢出了聊天室。"));
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => "已成功将 {$targetUser->username} 踢出房间。"]);
|
||||
return response()->json(['status' => 'success', 'message' => "已成功将 {$result['target']->username} 踢出房间。"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员/具有道具者操作:禁言 (对应新加的限制功能)
|
||||
* 禁言 (对应原版限制功能)
|
||||
* 所需等级由 sysparam level_mute 配置
|
||||
* 禁言信息存入 Redis,TTL 到期自动解除
|
||||
*/
|
||||
public function mute(Request $request, string $username): JsonResponse
|
||||
{
|
||||
$operator = Auth::user();
|
||||
$roomId = $request->input('room_id');
|
||||
$duration = $request->input('duration', 5); // 默认封停分钟数
|
||||
$duration = (int) $request->input('duration', 5);
|
||||
|
||||
if (! $roomId) {
|
||||
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
||||
}
|
||||
|
||||
$room = Room::findOrFail($roomId);
|
||||
|
||||
// 此处只做简单鉴权演示,和踢人一致
|
||||
if ($room->master !== $operator->username && $operator->user_level < 15) {
|
||||
return response()->json(['status' => 'error', 'message' => '权限不足,无法执行禁言操作。'], 403);
|
||||
$result = $this->checkPermission($operator, $username, $roomId, 'level_mute', '禁言');
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 后续可以在 Redis 中写入一个 `mute:{$username}` 并附带 `TTL` 以在后台拦截
|
||||
// 写入 Redis 禁言标记,TTL = 禁言分钟数 * 60
|
||||
Redis::setex("mute:{$roomId}:{$username}", $duration * 60, json_encode([
|
||||
'operator' => $operator->username,
|
||||
'reason' => '管理员禁言',
|
||||
'until' => now()->addMinutes($duration)->toDateTimeString(),
|
||||
]));
|
||||
|
||||
// 立刻向房间发送 Muted 事件
|
||||
// 广播禁言事件
|
||||
broadcast(new UserMuted($roomId, $username, $duration));
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => "已对 {$username} 实施封口 {$duration} 分钟。"]);
|
||||
return response()->json(['status' => 'success', 'message' => "已对 {$username} 实施禁言 {$duration} 分钟。"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 封号(禁止登录)
|
||||
* 所需等级由 sysparam level_ban 配置
|
||||
* 将用户等级设为 -1 表示封禁
|
||||
*/
|
||||
public function ban(Request $request, string $username): JsonResponse
|
||||
{
|
||||
$operator = Auth::user();
|
||||
$roomId = $request->input('room_id');
|
||||
|
||||
if (! $roomId) {
|
||||
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
||||
}
|
||||
|
||||
$result = $this->checkPermission($operator, $username, $roomId, 'level_ban', '封号');
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 封号:设置等级为 -1
|
||||
$result['target']->user_level = -1;
|
||||
$result['target']->save();
|
||||
|
||||
// 踢出聊天室
|
||||
broadcast(new UserKicked($roomId, $username, "管理员 [{$operator->username}] 已封禁用户 [{$username}] 的账号。"));
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号。"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 封IP(记录IP到黑名单并踢出)
|
||||
* 所需等级由 sysparam level_banip 配置
|
||||
*/
|
||||
public function banIp(Request $request, string $username): JsonResponse
|
||||
{
|
||||
$operator = Auth::user();
|
||||
$roomId = $request->input('room_id');
|
||||
|
||||
if (! $roomId) {
|
||||
return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
|
||||
}
|
||||
|
||||
$result = $this->checkPermission($operator, $username, $roomId, 'level_banip', '封IP');
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$targetIp = $result['target']->last_ip;
|
||||
|
||||
if ($targetIp) {
|
||||
// 将IP加入 Redis 黑名单(永久)
|
||||
Redis::sadd('banned_ips', $targetIp);
|
||||
}
|
||||
|
||||
// 同时封号
|
||||
$result['target']->user_level = -1;
|
||||
$result['target']->save();
|
||||
|
||||
// 踢出聊天室
|
||||
broadcast(new UserKicked($roomId, $username, "管理员 [{$operator->username}] 已封禁用户 [{$username}] 的IP地址。"));
|
||||
|
||||
$ipInfo = $targetIp ? "(IP: {$targetIp})" : '(未记录IP)';
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号并封IP{$ipInfo}。"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
/**
|
||||
* 文件功能:用户等级权限验证中间件
|
||||
* 支持传入固定数字等级 或 'super' 关键字(动态读取 sysparam 的 superlevel)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
@@ -10,6 +11,7 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Sysparam;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -18,21 +20,26 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
class LevelRequired
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* 校验当前登录用户的等级是否大于或等于要求等级。
|
||||
* 当 $level 为 'super' 时,动态从 sysparam 表读取 superlevel 值。
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
* @param int $level 要求达到的最低等级 (例如 15 为室主)
|
||||
* @param string $level 要求的最低等级(数字 或 'super')
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, int $level): Response
|
||||
public function handle(Request $request, Closure $next, string $level = 'super'): Response
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return redirect()->route('home');
|
||||
}
|
||||
|
||||
// 动态解析等级要求:'super' → 从 sysparam 读取,数字 → 直接使用
|
||||
$requiredLevel = ($level === 'super')
|
||||
? (int) Sysparam::getValue('superlevel', '100')
|
||||
: (int) $level;
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user->user_level < $level) {
|
||||
if ($user->user_level < $requiredLevel) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => '权限不足', 'status' => 'error'], 403);
|
||||
}
|
||||
|
||||
@@ -39,8 +39,7 @@ class LoginRequest extends FormRequest
|
||||
'regex:/^[^<>\'"]+$/u',
|
||||
],
|
||||
'password' => ['required', 'string', 'min:1'],
|
||||
// 'captcha' => ['required', 'captcha'],
|
||||
'captcha' => ['nullable'], // 【本地调试临时绕过验证码强校验,跳过session丢失问题】
|
||||
'captcha' => ['required', 'captcha'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
57
app/Models/Autoact.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:自动动作事件模型
|
||||
* 对应原版 ASP 聊天室的 autoact 表
|
||||
* 存储系统随机事件(好运/坏运/经验/金币奖惩)
|
||||
* 管理员可在后台管理这些事件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Autoact extends Model
|
||||
{
|
||||
/** @var string 表名 */
|
||||
protected $table = 'autoact';
|
||||
|
||||
/** @var array 可批量赋值的字段 */
|
||||
protected $fillable = [
|
||||
'text_body',
|
||||
'event_type',
|
||||
'exp_change',
|
||||
'jjb_change',
|
||||
'enabled',
|
||||
];
|
||||
|
||||
/** @var array 类型转换 */
|
||||
protected $casts = [
|
||||
'exp_change' => 'integer',
|
||||
'jjb_change' => 'integer',
|
||||
'enabled' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 随机获取一条已启用的事件
|
||||
*/
|
||||
public static function randomEvent(): ?self
|
||||
{
|
||||
return static::where('enabled', true)->inRandomOrder()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将事件文本中的变量替换为实际值
|
||||
*
|
||||
* @param string $username 当前用户名
|
||||
* @return string 替换后的文本
|
||||
*/
|
||||
public function renderText(string $username): string
|
||||
{
|
||||
return str_replace('{username}', $username, $this->text_body);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ class Room extends Model
|
||||
'build_time',
|
||||
'permit_level',
|
||||
'door_open',
|
||||
'announcement',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,28 +2,89 @@
|
||||
|
||||
/**
|
||||
* 文件功能:系统参数模型
|
||||
* 对应原版 ASP 聊天室的 sysparam 配置表
|
||||
* 管理员可在后台修改等级经验阈值等系统参数
|
||||
*
|
||||
* 对应原 ASP 文件:sysparam 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @package App\Models
|
||||
* @author ChatRoom Laravel
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SysParam extends Model
|
||||
class Sysparam extends Model
|
||||
{
|
||||
/** @var string 表名 */
|
||||
protected $table = 'sysparam';
|
||||
|
||||
/** @var array 可批量赋值的字段 */
|
||||
protected $fillable = ['alias', 'body', 'guidetxt'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
* 获取指定参数的值
|
||||
* 带缓存,避免频繁查库
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @param string $alias 参数别名
|
||||
* @param string $default 默认值
|
||||
* @return string
|
||||
*/
|
||||
protected $fillable = [
|
||||
'alias',
|
||||
'guidetxt',
|
||||
'body',
|
||||
];
|
||||
public static function getValue(string $alias, string $default = ''): string
|
||||
{
|
||||
return Cache::remember("sysparam:{$alias}", 300, function () use ($alias, $default) {
|
||||
$param = static::where('alias', $alias)->first();
|
||||
|
||||
return $param ? ($param->body ?? $default) : $default;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取等级经验阈值数组
|
||||
* 返回格式:[0 => 10, 1 => 50, 2 => 150, ...] 索引即为目标等级-1
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public static function getLevelExpThresholds(): array
|
||||
{
|
||||
$str = static::getValue('levelexp', '10,50,150,400,800,1500,3000,5000,8000,12000,18000,25000,35000,50000,80000');
|
||||
|
||||
return array_map('intval', explode(',', $str));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据经验值计算应该达到的等级
|
||||
*
|
||||
* @param int $expNum 当前经验值
|
||||
* @return int 对应的等级
|
||||
*/
|
||||
public static function calculateLevel(int $expNum): int
|
||||
{
|
||||
$thresholds = static::getLevelExpThresholds();
|
||||
$level = 0;
|
||||
|
||||
foreach ($thresholds as $threshold) {
|
||||
if ($expNum >= $threshold) {
|
||||
$level++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 不超过最大等级
|
||||
$maxLevel = (int) static::getValue('maxlevel', '99');
|
||||
|
||||
return min($level, $maxLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定参数的缓存
|
||||
*
|
||||
* @param string $alias 参数别名
|
||||
*/
|
||||
public static function clearCache(string $alias): void
|
||||
{
|
||||
Cache::forget("sysparam:{$alias}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
@@ -34,6 +35,7 @@ class User extends Authenticatable
|
||||
'room_id',
|
||||
'first_ip',
|
||||
'last_ip',
|
||||
'usersf',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -69,4 +71,18 @@ class User extends Authenticatable
|
||||
'q3_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像文件名访问器
|
||||
*
|
||||
* 原 ASP 系统的头像文件名存储在 usersf 字段中(如 "75.GIF"),
|
||||
* 但项目中各处通过 $user->headface 来引用头像。
|
||||
* 此 accessor 将 headface 属性映射到 usersf 字段,保持代码一致性。
|
||||
*/
|
||||
protected function headface(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->usersf ?: '1.GIF',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
assets/backgrounds/01.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/backgrounds/02.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/backgrounds/03.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
assets/backgrounds/04.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/backgrounds/05.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
assets/backgrounds/06.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/backgrounds/07.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/backgrounds/08.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/backgrounds/09.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/backgrounds/10.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/backgrounds/11.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
assets/backgrounds/12.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
assets/fonts/ABeeZee_regular.ttf
Normal file
BIN
assets/fonts/Asap_700.ttf
Normal file
BIN
assets/fonts/Khand_500.ttf
Normal file
BIN
assets/fonts/Open_Sans_regular.ttf
Normal file
BIN
assets/fonts/Roboto_regular.ttf
Normal file
BIN
assets/fonts/Ubuntu_regular.ttf
Normal file
202
assets/fonts/license/LICENSE-2.0.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
1
assets/fonts/license/OFL.txt
Normal file
@@ -0,0 +1 @@
|
||||
Copyright (c) <dates>, <Copyright Holder> (<URL|email>),
|
||||
96
assets/fonts/license/ubuntu-font-licence-1.0.txt
Normal file
@@ -0,0 +1,96 @@
|
||||
-------------------------------
|
||||
UBUNTU FONT LICENCE Version 1.0
|
||||
-------------------------------
|
||||
|
||||
PREAMBLE
|
||||
This licence allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely. The fonts, including any derivative works, can be
|
||||
bundled, embedded, and redistributed provided the terms of this licence
|
||||
are met. The fonts and derivatives, however, cannot be released under
|
||||
any other licence. The requirement for fonts to remain under this
|
||||
licence does not require any document created using the fonts or their
|
||||
derivatives to be published under this licence, as long as the primary
|
||||
purpose of the document is not to be a vehicle for the distribution of
|
||||
the fonts.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this licence and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Original Version" refers to the collection of Font Software components
|
||||
as received under this licence.
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to
|
||||
a new environment.
|
||||
|
||||
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||
copyright ownership of the Font Software.
|
||||
|
||||
"Substantially Changed" refers to Modified Versions which can be easily
|
||||
identified as dissimilar to the Font Software by users of the Font
|
||||
Software comparing the Original Version with the Modified Version.
|
||||
|
||||
To "Propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification and with or without charging
|
||||
a redistribution fee), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
This licence does not grant any rights under trademark law and all such
|
||||
rights are reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of the Font Software, to propagate the Font Software, subject to
|
||||
the below conditions:
|
||||
|
||||
1) Each copy of the Font Software must contain the above copyright
|
||||
notice and this licence. These can be included either as stand-alone
|
||||
text files, human-readable headers or in the appropriate machine-
|
||||
readable metadata fields within text or binary files as long as those
|
||||
fields can be easily viewed by the user.
|
||||
|
||||
2) The font name complies with the following:
|
||||
(a) The Original Version must retain its name, unmodified.
|
||||
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||
avoid use of the name of the Original Version or similar names entirely.
|
||||
(c) Modified Versions which are not Substantially Changed must be
|
||||
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||
additional naming elements to distinguish the Modified Version from the
|
||||
Original Version. The name of such Modified Versions must be the name of
|
||||
the Original Version, with "derivative X" where X represents the name of
|
||||
the new work, appended to that name.
|
||||
|
||||
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||
Font Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except (i) as required by this licence, (ii) to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||
their explicit written permission.
|
||||
|
||||
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||
be distributed entirely under this licence, and must not be distributed
|
||||
under any other licence. The requirement for fonts to remain under this
|
||||
licence does not affect any document created using the Font Software,
|
||||
except any version of the Font Software extracted from a document
|
||||
created using the Font Software may only be distributed under this
|
||||
licence.
|
||||
|
||||
TERMINATION
|
||||
This licence becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||
DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
return [
|
||||
'disable' => env('CAPTCHA_DISABLE', false),
|
||||
'characters' => ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
||||
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
|
||||
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
|
||||
't', 'u', 'v', 'w', 'x', 'y', 'z', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
'characters' => [2, 3, 4, 5, 6, 7, 8, 9],
|
||||
'fontsDirectory' => dirname(__DIR__).'/assets/fonts',
|
||||
'bgsDirectory' => dirname(__DIR__).'/assets/backgrounds',
|
||||
'default' => [
|
||||
'length' => 6,
|
||||
'width' => 345,
|
||||
'height' => 65,
|
||||
'length' => 4,
|
||||
'width' => 120,
|
||||
'height' => 36,
|
||||
'quality' => 90,
|
||||
'math' => false,
|
||||
'expire' => 60,
|
||||
'encrypt' => false,
|
||||
'lines' => 2,
|
||||
'bgImage' => false,
|
||||
'bgColor' => '#ffffff',
|
||||
],
|
||||
'flat' => [
|
||||
'length' => 6,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:为 rooms 表添加 visit_num(人气/访问量)字段
|
||||
* 用于记录房间的总访问人次,每次用户进入房间时递增
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 添加 visit_num 字段到 rooms 表
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('rooms', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('visit_num')->default(0)->after('ooooo')
|
||||
->comment('房间人气(总访问人次)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:移除 visit_num 字段
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('rooms', function (Blueprint $table) {
|
||||
$table->dropColumn('visit_num');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:创建系统参数表(sysparam)
|
||||
* 复刻原版 ASP 聊天室的 sysparam 系统配置表
|
||||
* 存储等级-经验阈值、系统开关等管理员可配置项
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 创建 sysparam 表并插入默认等级经验配置
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sysparam', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('alias', 100)->unique()->comment('参数别名(唯一标识)');
|
||||
$table->text('body')->nullable()->comment('参数值');
|
||||
$table->string('guidetxt', 500)->nullable()->comment('参数说明(后台显示)');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 插入默认的等级经验配置(管理员可在后台修改)
|
||||
// 格式:逗号分隔,每项为"等级所需的累计经验值"
|
||||
// 例如:等级1需要10经验,等级2需要50,等级3需要150...
|
||||
DB::table('sysparam')->insert([
|
||||
[
|
||||
'alias' => 'levelexp',
|
||||
'body' => '10,50,150,400,800,1500,3000,5000,8000,12000,18000,25000,35000,50000,80000',
|
||||
'guidetxt' => '等级经验阈值配置(逗号分隔)。第N个数字表示升到N级所需的累计经验值。例如:10,50,150 表示1级需10经验,2级需50经验,3级需150经验。',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'alias' => 'exp_per_heartbeat',
|
||||
'body' => '1',
|
||||
'guidetxt' => '每次心跳(约60秒)增加的经验值',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'alias' => 'superlevel',
|
||||
'body' => '16',
|
||||
'guidetxt' => '管理员级别(= 最高等级 + 1,拥有最高权限的等级阈值)',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'alias' => 'maxlevel',
|
||||
'body' => '15',
|
||||
'guidetxt' => '用户最高可达等级',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:删除 sysparam 表
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sysparam');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:创建自动动作事件表(autoact)
|
||||
* 复刻原版 ASP 聊天室的 autoact 表
|
||||
* 存储系统随机事件(好运/坏运/经验/金币奖惩等)
|
||||
* 管理员可在后台增删改这些事件
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 创建 autoact 表并插入默认随机事件
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('autoact', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('text_body', 500)->comment('事件文本内容(支持变量替换)');
|
||||
$table->string('event_type', 20)->default('neutral')->comment('事件类型:good=好运, bad=坏运, neutral=中性');
|
||||
$table->integer('exp_change')->default(0)->comment('经验变化值(正=奖励, 负=惩罚)');
|
||||
$table->integer('jjb_change')->default(0)->comment('金币变化值(正=奖励, 负=惩罚)');
|
||||
$table->boolean('enabled')->default(true)->comment('是否启用');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 插入默认随机事件
|
||||
$events = [
|
||||
// 好运事件(奖励经验/金币)
|
||||
['text_body' => '🎉 恭喜【{username}】路遇仙人指点,获得 100 经验值!', 'event_type' => 'good', 'exp_change' => 100, 'jjb_change' => 0],
|
||||
['text_body' => '💰 【{username}】在路边捡到一袋金币,获得 500 金币!', 'event_type' => 'good', 'exp_change' => 0, 'jjb_change' => 500],
|
||||
['text_body' => '🌟 天降祥瑞!【{username}】获得 200 经验 + 200 金币!', 'event_type' => 'good', 'exp_change' => 200, 'jjb_change' => 200],
|
||||
['text_body' => '🎊 【{username}】参加武林大会获胜,奖励 300 经验!', 'event_type' => 'good', 'exp_change' => 300, 'jjb_change' => 0],
|
||||
['text_body' => '🏆 【{username}】完成了一个神秘任务,获得 150 金币!', 'event_type' => 'good', 'exp_change' => 0, 'jjb_change' => 150],
|
||||
['text_body' => '✨ 【{username}】领悟了一招绝学,经验暴增 500!', 'event_type' => 'good', 'exp_change' => 500, 'jjb_change' => 0],
|
||||
['text_body' => '🎁 系统随机赠送【{username}】100 金币,请查收!', 'event_type' => 'good', 'exp_change' => 0, 'jjb_change' => 100],
|
||||
|
||||
// 坏运事件(扣除经验/金币)
|
||||
['text_body' => '💀 【{username}】不小心踩到陷阱,损失 50 经验!', 'event_type' => 'bad', 'exp_change' => -50, 'jjb_change' => 0],
|
||||
['text_body' => '😱 【{username}】遭遇山贼打劫,被抢走 100 金币!', 'event_type' => 'bad', 'exp_change' => 0, 'jjb_change' => -100],
|
||||
['text_body' => '🌧️ 【{username}】在雨中迷路,消耗 80 经验值。', 'event_type' => 'bad', 'exp_change' => -80, 'jjb_change' => 0],
|
||||
['text_body' => '🐍 【{username}】被毒蛇咬了一口,损失 30 经验和 50 金币!', 'event_type' => 'bad', 'exp_change' => -30, 'jjb_change' => -50],
|
||||
|
||||
// 中性事件(纯文字,不奖惩)
|
||||
['text_body' => '🔮 星海小博士:【{username}】今天运势不错,继续加油!', 'event_type' => 'neutral', 'exp_change' => 0, 'jjb_change' => 0],
|
||||
['text_body' => '📜 星海小博士:有人说"坚持就是胜利",【{username}】觉得呢?', 'event_type' => 'neutral', 'exp_change' => 0, 'jjb_change' => 0],
|
||||
['text_body' => '🌈 星海小博士:【{username}】今天心情如何?记得多喝水哦!', 'event_type' => 'neutral', 'exp_change' => 0, 'jjb_change' => 0],
|
||||
['text_body' => '🎵 星海小博士:送给【{username}】一首歌,祝你天天开心!', 'event_type' => 'neutral', 'exp_change' => 0, 'jjb_change' => 0],
|
||||
];
|
||||
|
||||
$now = now();
|
||||
foreach ($events as &$event) {
|
||||
$event['enabled'] = true;
|
||||
$event['created_at'] = $now;
|
||||
$event['updated_at'] = $now;
|
||||
}
|
||||
|
||||
DB::table('autoact')->insert($events);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:删除 autoact 表
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('autoact');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:为 rooms 表添加 announcement(公告/祝福语)字段
|
||||
* 有权限的用户可以设置房间公告,显示在聊天室顶部滚动条
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 添加 announcement 字段
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('rooms', function (Blueprint $table) {
|
||||
$table->string('announcement', 500)->nullable()->after('room_des')->comment('房间公告/祝福语(滚动显示)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('rooms', function (Blueprint $table) {
|
||||
$table->dropColumn('announcement');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:将 users 表的 s_color 字段从 integer 改为 varchar
|
||||
* 以便直接存储 hex 颜色字符串(如 #ff0000),与前端颜色选择器格式一致
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 执行迁移:s_color 从 integer 改为 varchar(10)
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('s_color', 10)->nullable()->comment('发言颜色(hex,如 #ff0000)')->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:s_color 从 varchar 改回 integer
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->integer('s_color')->nullable()->comment('发言颜色')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:补充缺失的 sysparam 系统参数记录
|
||||
* 代码中引用了多个参数但原始迁移只种子了 4 条,
|
||||
* 此迁移使用 insertOrIgnore 补充缺失的参数配置
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 补充缺失的系统参数记录(使用 insertOrIgnore 避免重复)
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$now = now();
|
||||
|
||||
DB::table('sysparam')->insertOrIgnore([
|
||||
[
|
||||
'alias' => 'level_kick',
|
||||
'body' => '10',
|
||||
'guidetxt' => '踢人所需等级(管理员可在聊天室踢出用户的最低等级)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'level_mute',
|
||||
'body' => '8',
|
||||
'guidetxt' => '禁言所需等级(管理员可在聊天室禁言用户的最低等级)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'level_ban',
|
||||
'body' => '12',
|
||||
'guidetxt' => '封号所需等级(管理员可封禁用户账号的最低等级)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'level_banip',
|
||||
'body' => '14',
|
||||
'guidetxt' => '封IP所需等级(管理员可封禁用户IP地址的最低等级)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'level_announcement',
|
||||
'body' => '10',
|
||||
'guidetxt' => '设置公告所需等级(可修改房间公告/祝福语的最低等级)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'auto_event_chance',
|
||||
'body' => '10',
|
||||
'guidetxt' => '随机事件触发概率(百分比,1-100,每次心跳时按此概率触发随机事件)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:删除补充的参数记录
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('sysparam')->whereIn('alias', [
|
||||
'level_kick',
|
||||
'level_mute',
|
||||
'level_ban',
|
||||
'level_banip',
|
||||
'level_announcement',
|
||||
'auto_event_chance',
|
||||
])->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:将等级系统从 15 级扩展到 99 级
|
||||
* - maxlevel: 15 → 99(用户最高等级)
|
||||
* - superlevel: 16 → 100(管理员等级)
|
||||
* - levelexp: 重新生成 99 级的经验阶梯
|
||||
* - 管理权限等级按比例调整到新体系
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 更新等级系统参数至 99 级体系
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 生成 99 级经验阶梯(使用 level^2.5 的幂次曲线,确保平滑递增)
|
||||
$thresholds = [];
|
||||
for ($level = 1; $level <= 99; $level++) {
|
||||
// 公式:10 * level^2.5,取整到十位数,保持数据整洁
|
||||
$exp = (int) (ceil(10 * pow($level, 2.5) / 10) * 10);
|
||||
$thresholds[] = $exp;
|
||||
}
|
||||
$levelExpStr = implode(',', $thresholds);
|
||||
|
||||
$now = now();
|
||||
|
||||
// 更新核心等级参数
|
||||
DB::table('sysparam')->where('alias', 'maxlevel')
|
||||
->update(['body' => '99', 'guidetxt' => '用户最高可达等级', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'superlevel')
|
||||
->update(['body' => '100', 'guidetxt' => '管理员级别(= 最高等级 + 1,拥有最高权限的等级阈值)', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'levelexp')
|
||||
->update(['body' => $levelExpStr, 'updated_at' => $now]);
|
||||
|
||||
// 按比例调整管理权限等级(原体系 /15,新体系 /99)
|
||||
// 禁言:8/15 ≈ 53% → 50级
|
||||
DB::table('sysparam')->where('alias', 'level_mute')
|
||||
->update(['body' => '50', 'updated_at' => $now]);
|
||||
|
||||
// 踢人:10/15 ≈ 67% → 60级
|
||||
DB::table('sysparam')->where('alias', 'level_kick')
|
||||
->update(['body' => '60', 'updated_at' => $now]);
|
||||
|
||||
// 设公告:10/15 ≈ 67% → 60级
|
||||
DB::table('sysparam')->where('alias', 'level_announcement')
|
||||
->update(['body' => '60', 'updated_at' => $now]);
|
||||
|
||||
// 封号:12/15 = 80% → 80级
|
||||
DB::table('sysparam')->where('alias', 'level_ban')
|
||||
->update(['body' => '80', 'updated_at' => $now]);
|
||||
|
||||
// 封IP:14/15 ≈ 93% → 90级
|
||||
DB::table('sysparam')->where('alias', 'level_banip')
|
||||
->update(['body' => '90', 'updated_at' => $now]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:恢复到原始 15 级体系
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$now = now();
|
||||
|
||||
DB::table('sysparam')->where('alias', 'maxlevel')
|
||||
->update(['body' => '15', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'superlevel')
|
||||
->update(['body' => '16', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'levelexp')
|
||||
->update(['body' => '10,50,150,400,800,1500,3000,5000,8000,12000,18000,25000,35000,50000,80000', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'level_mute')
|
||||
->update(['body' => '8', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'level_kick')
|
||||
->update(['body' => '10', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'level_announcement')
|
||||
->update(['body' => '10', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'level_ban')
|
||||
->update(['body' => '12', 'updated_at' => $now]);
|
||||
|
||||
DB::table('sysparam')->where('alias', 'level_banip')
|
||||
->update(['body' => '14', 'updated_at' => $now]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:添加钓鱼系统参数到 sysparam 表
|
||||
* 配置钓鱼花费金币、冷却时间、等待上钩时间范围
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 插入钓鱼相关系统参数
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$now = now();
|
||||
|
||||
DB::table('sysparam')->insertOrIgnore([
|
||||
[
|
||||
'alias' => 'fishing_cost',
|
||||
'body' => '5',
|
||||
'guidetxt' => '每次钓鱼花费金币数量',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'fishing_cooldown',
|
||||
'body' => '300',
|
||||
'guidetxt' => '钓鱼冷却时间(秒,默认300秒=5分钟)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'fishing_wait_min',
|
||||
'body' => '8',
|
||||
'guidetxt' => '钓鱼最短等待上钩时间(秒)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'alias' => 'fishing_wait_max',
|
||||
'body' => '15',
|
||||
'guidetxt' => '钓鱼最长等待上钩时间(秒)',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚:删除钓鱼参数
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('sysparam')->whereIn('alias', [
|
||||
'fishing_cost',
|
||||
'fishing_cooldown',
|
||||
'fishing_wait_min',
|
||||
'fishing_wait_max',
|
||||
])->delete();
|
||||
}
|
||||
};
|
||||
629
public/css/chat.css
Normal file
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* 文件功能:聊天室主界面样式表
|
||||
* 复刻原版海军蓝聊天室色系 (CHAT.CSS)
|
||||
* 包含布局、消息窗格、输入工具栏、用户列表、弹窗等所有聊天界面样式
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
原版海军蓝聊天室色系 (CHAT.CSS 复刻)
|
||||
═══════════════════════════════════════════════════ */
|
||||
:root {
|
||||
--bg-main: #EAF2FF;
|
||||
--bg-bar: #b0d8ff;
|
||||
--bg-header: #4488aa;
|
||||
--bg-toolbar: #5599aa;
|
||||
--text-link: #336699;
|
||||
--text-navy: #112233;
|
||||
--text-white: #ffffff;
|
||||
--border-blue: #336699;
|
||||
--scrollbar-base: #b0d8ff;
|
||||
--scrollbar-arrow: #336699;
|
||||
--scrollbar-track: #EAF2FF;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "SimSun", "宋体", sans-serif;
|
||||
font-size: 10pt;
|
||||
background: var(--bg-main);
|
||||
color: var(--text-navy);
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 全局链接样式 */
|
||||
a {
|
||||
color: var(--text-link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #009900;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 - 模拟原版 */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-base);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--scrollbar-arrow);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--scrollbar-track);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: var(--scrollbar-base);
|
||||
}
|
||||
|
||||
/* ── 主布局 ─────────────────────────────────────── */
|
||||
.chat-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* 左侧主区域 */
|
||||
.chat-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 竖向工具条 */
|
||||
.chat-toolbar {
|
||||
width: 28px;
|
||||
background: var(--bg-toolbar);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 40px 0 2px 0;
|
||||
border-left: 1px solid var(--border-blue);
|
||||
border-right: 1px solid var(--border-blue);
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-toolbar .tool-btn {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
font-size: 11px;
|
||||
color: #ddd;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
transition: all 0.15s;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.chat-toolbar .tool-btn:hover {
|
||||
color: #fff;
|
||||
background: #5599cc;
|
||||
}
|
||||
|
||||
/* 右侧用户列表面板 */
|
||||
.chat-right {
|
||||
width: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid var(--border-blue);
|
||||
background: var(--bg-main);
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
/* ── 标题栏 ─────────────────────────────────────── */
|
||||
.room-title-bar {
|
||||
background: var(--bg-header);
|
||||
color: var(--text-white);
|
||||
text-align: center;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--border-blue);
|
||||
}
|
||||
|
||||
.room-title-bar .room-name {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.room-title-bar .room-desc {
|
||||
color: #cee8ff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 公告/祝福语滚动条(复刻原版 marquee) */
|
||||
.room-announcement-bar {
|
||||
background: linear-gradient(to right, #a8d8a8, #c8f0c8, #a8d8a8);
|
||||
padding: 2px 6px;
|
||||
border-bottom: 1px solid var(--border-blue);
|
||||
flex-shrink: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── 消息窗格 ───────────────────────────────────── */
|
||||
.message-panes {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.message-pane {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 5px 8px;
|
||||
background: var(--bg-main);
|
||||
font-size: 10pt;
|
||||
line-height: 170%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.message-pane.say1 {
|
||||
border-bottom: 2px solid var(--border-blue);
|
||||
}
|
||||
|
||||
.message-pane.say2 {
|
||||
display: block;
|
||||
flex: 0.4;
|
||||
border-top: 2px solid var(--border-blue);
|
||||
}
|
||||
|
||||
.message-pane.say1 {
|
||||
flex: 0.6;
|
||||
}
|
||||
|
||||
/* 分屏模式下两个窗格各占一半 */
|
||||
.message-panes.split-h .message-pane.say1,
|
||||
.message-panes.split-h .message-pane.say2 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-panes.split-h .message-pane.say2 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-panes.split-h .message-pane.say1 {
|
||||
flex: 1;
|
||||
border-bottom: 2px solid var(--border-blue);
|
||||
}
|
||||
|
||||
/* 消息行样式 */
|
||||
.msg-line {
|
||||
margin: 2px 0;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.msg-line .msg-time {
|
||||
font-size: 9px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.msg-line .msg-user {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.msg-line .msg-user:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.msg-line .msg-action {
|
||||
color: #009900;
|
||||
}
|
||||
|
||||
.msg-line .msg-secret {
|
||||
color: #cc00cc;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.msg-line .msg-content {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.msg-line.sys-msg {
|
||||
color: #cc0000;
|
||||
text-align: center;
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
/* ── 底部输入工具栏 ─────────────────────────────── */
|
||||
.input-bar {
|
||||
height: auto;
|
||||
min-height: 75px;
|
||||
background: var(--bg-bar);
|
||||
border-top: 2px solid var(--border-blue);
|
||||
padding: 3px 6px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.input-bar select,
|
||||
.input-bar input[type="text"],
|
||||
.input-bar input[type="checkbox"] {
|
||||
font-size: 12px;
|
||||
border: 1px solid navy;
|
||||
color: #224466;
|
||||
padding: 1px 2px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.input-bar select {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.input-bar .input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-bar .say-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
font-size: 12px;
|
||||
border: 1px solid navy;
|
||||
color: #224466;
|
||||
padding: 3px 5px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-bar .say-input:focus {
|
||||
border-color: #0066cc;
|
||||
box-shadow: 0 0 3px rgba(0, 102, 204, 0.3);
|
||||
}
|
||||
|
||||
.input-bar .send-btn {
|
||||
background: var(--bg-header);
|
||||
color: white;
|
||||
border: 1px solid var(--border-blue);
|
||||
padding: 3px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.input-bar .send-btn:hover {
|
||||
background: #336699;
|
||||
}
|
||||
|
||||
.input-bar .send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input-bar label {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* ── 右侧用户列表 Tab 栏 ──────────────────────── */
|
||||
.right-tabs {
|
||||
display: flex;
|
||||
background: var(--bg-bar);
|
||||
border-bottom: 2px solid navy;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.right-tabs .tab-btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding: 3px 0;
|
||||
cursor: pointer;
|
||||
color: navy;
|
||||
background: var(--bg-bar);
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.right-tabs .tab-btn.active {
|
||||
background: navy;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.right-tabs .tab-btn:hover:not(.active) {
|
||||
background: #99c8ee;
|
||||
}
|
||||
|
||||
/* 用户列表内容 */
|
||||
.user-list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px dotted #cde;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.user-item:hover {
|
||||
background: #d0e8ff;
|
||||
}
|
||||
|
||||
.user-item .user-head {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 2px;
|
||||
object-fit: cover;
|
||||
background: #ddd;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-item .user-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-link);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-item .user-sex {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.user-item .user-level {
|
||||
font-size: 9px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 在线人数统计 */
|
||||
.online-stats {
|
||||
background: var(--bg-bar);
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
padding: 3px;
|
||||
border-top: 1px solid var(--border-blue);
|
||||
color: navy;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── 用户名片弹窗 ───────────────────────────────── */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background: white;
|
||||
border: 2px solid var(--border-blue);
|
||||
border-radius: 8px;
|
||||
width: 340px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, var(--bg-header), var(--border-blue));
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal-body .profile-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-body .profile-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--bg-bar);
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.modal-body .profile-info h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--text-navy);
|
||||
}
|
||||
|
||||
.modal-body .profile-info .level-badge {
|
||||
display: inline-block;
|
||||
background: var(--bg-bar);
|
||||
color: var(--border-blue);
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.modal-body .profile-info .sex-badge {
|
||||
font-size: 11px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.modal-body .profile-detail {
|
||||
background: #f5f9ff;
|
||||
border: 1px solid #e0ecff;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
padding: 0 16px 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-actions button,
|
||||
.modal-actions a {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border: 1px solid;
|
||||
transition: all 0.15s;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn-kick {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
border-color: #fcc;
|
||||
}
|
||||
|
||||
.btn-kick:hover {
|
||||
background: #fcc;
|
||||
}
|
||||
|
||||
.btn-mute {
|
||||
background: #fff8e6;
|
||||
color: #b86e00;
|
||||
border-color: #ffe0a0;
|
||||
}
|
||||
|
||||
.btn-mute:hover {
|
||||
background: #ffe0a0;
|
||||
}
|
||||
|
||||
.btn-mail {
|
||||
background: #f0e8ff;
|
||||
color: #6633cc;
|
||||
border-color: #d8c8ff;
|
||||
}
|
||||
|
||||
.btn-mail:hover {
|
||||
background: #d8c8ff;
|
||||
}
|
||||
|
||||
.btn-whisper {
|
||||
background: #e8f5ff;
|
||||
color: var(--text-link);
|
||||
border-color: var(--bg-bar);
|
||||
}
|
||||
|
||||
.btn-whisper:hover {
|
||||
background: var(--bg-bar);
|
||||
}
|
||||
|
||||
/* 禁言输入行 */
|
||||
.mute-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.mute-form input {
|
||||
width: 60px;
|
||||
border: 1px solid #ffe0a0;
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mute-form button {
|
||||
background: #b86e00;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── 头像选择器 ─────────────────────────────────── */
|
||||
.avatar-option {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 3px;
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.avatar-option:hover {
|
||||
border-color: #88bbdd;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.avatar-option.selected {
|
||||
border-color: #336699;
|
||||
box-shadow: 0 0 6px rgba(51, 102, 153, 0.5);
|
||||
}
|
||||
BIN
public/images/headface/084.gif
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/images/headface/086.gif
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/headface/1.GIF
Normal file
|
After Width: | Height: | Size: 335 B |
BIN
public/images/headface/10.GIF
Normal file
|
After Width: | Height: | Size: 324 B |
BIN
public/images/headface/100.GIF
Normal file
|
After Width: | Height: | Size: 292 B |
BIN
public/images/headface/10001.GIF
Normal file
|
After Width: | Height: | Size: 425 B |
BIN
public/images/headface/10002.GIF
Normal file
|
After Width: | Height: | Size: 410 B |
BIN
public/images/headface/10003.GIF
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
public/images/headface/10004.GIF
Normal file
|
After Width: | Height: | Size: 377 B |
BIN
public/images/headface/10005.GIF
Normal file
|
After Width: | Height: | Size: 255 B |
BIN
public/images/headface/10006.GIF
Normal file
|
After Width: | Height: | Size: 372 B |
BIN
public/images/headface/10008.GIF
Normal file
|
After Width: | Height: | Size: 246 B |
BIN
public/images/headface/10009.GIF
Normal file
|
After Width: | Height: | Size: 389 B |
BIN
public/images/headface/1001.gif
Normal file
|
After Width: | Height: | Size: 823 B |
BIN
public/images/headface/10010.GIF
Normal file
|
After Width: | Height: | Size: 399 B |
BIN
public/images/headface/10011.GIF
Normal file
|
After Width: | Height: | Size: 292 B |
BIN
public/images/headface/10012.GIF
Normal file
|
After Width: | Height: | Size: 245 B |
BIN
public/images/headface/10013.GIF
Normal file
|
After Width: | Height: | Size: 428 B |
BIN
public/images/headface/10015.GIF
Normal file
|
After Width: | Height: | Size: 471 B |
BIN
public/images/headface/10016.GIF
Normal file
|
After Width: | Height: | Size: 241 B |
BIN
public/images/headface/10017.GIF
Normal file
|
After Width: | Height: | Size: 749 B |
BIN
public/images/headface/10019.GIF
Normal file
|
After Width: | Height: | Size: 315 B |
BIN
public/images/headface/10020.GIF
Normal file
|
After Width: | Height: | Size: 388 B |
BIN
public/images/headface/10021.GIF
Normal file
|
After Width: | Height: | Size: 597 B |
BIN
public/images/headface/10022.GIF
Normal file
|
After Width: | Height: | Size: 461 B |
BIN
public/images/headface/10024.GIF
Normal file
|
After Width: | Height: | Size: 310 B |
BIN
public/images/headface/10025.GIF
Normal file
|
After Width: | Height: | Size: 312 B |
BIN
public/images/headface/10026.GIF
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
public/images/headface/10028.GIF
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
public/images/headface/10029.GIF
Normal file
|
After Width: | Height: | Size: 304 B |
BIN
public/images/headface/10031.GIF
Normal file
|
After Width: | Height: | Size: 474 B |
BIN
public/images/headface/10032.GIF
Normal file
|
After Width: | Height: | Size: 322 B |
BIN
public/images/headface/10033.GIF
Normal file
|
After Width: | Height: | Size: 267 B |
BIN
public/images/headface/10035.GIF
Normal file
|
After Width: | Height: | Size: 344 B |
BIN
public/images/headface/10036.GIF
Normal file
|
After Width: | Height: | Size: 559 B |
BIN
public/images/headface/10037.GIF
Normal file
|
After Width: | Height: | Size: 253 B |
BIN
public/images/headface/10038.GIF
Normal file
|
After Width: | Height: | Size: 380 B |
BIN
public/images/headface/10039.GIF
Normal file
|
After Width: | Height: | Size: 342 B |
BIN
public/images/headface/10041.GIF
Normal file
|
After Width: | Height: | Size: 627 B |
BIN
public/images/headface/10043.GIF
Normal file
|
After Width: | Height: | Size: 284 B |
BIN
public/images/headface/10044.GIF
Normal file
|
After Width: | Height: | Size: 363 B |
BIN
public/images/headface/10045.GIF
Normal file
|
After Width: | Height: | Size: 179 B |
BIN
public/images/headface/10046.GIF
Normal file
|
After Width: | Height: | Size: 288 B |
BIN
public/images/headface/10047.GIF
Normal file
|
After Width: | Height: | Size: 324 B |
BIN
public/images/headface/10049.GIF
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
public/images/headface/10050.GIF
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
public/images/headface/10051.GIF
Normal file
|
After Width: | Height: | Size: 288 B |
BIN
public/images/headface/10052.GIF
Normal file
|
After Width: | Height: | Size: 327 B |
BIN
public/images/headface/10053.GIF
Normal file
|
After Width: | Height: | Size: 330 B |
BIN
public/images/headface/10054.GIF
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
public/images/headface/10055.GIF
Normal file
|
After Width: | Height: | Size: 326 B |
BIN
public/images/headface/10056.GIF
Normal file
|
After Width: | Height: | Size: 392 B |
BIN
public/images/headface/10057.GIF
Normal file
|
After Width: | Height: | Size: 293 B |