功能:钓鱼游戏后台管理系统

一、钓鱼全局开关
- 钓鱼纳入 GameConfig(game_key=fishing),游戏管理页可一键开关
- cast() 接口加开关校验,关闭时返回 403 友好提示
- GameConfigSeeder 新增 fishing 配置(含4个参数)

二、钓鱼事件数据库化
- 新建 fishing_events 表(emoji/name/message/exp/jjb/weight/is_active/sort)
- FishingEvent 模型含 rollOne() 加权随机方法
- FishingEventSeeder 填充7条初始事件(经验降低、金币提升)
- FishingController::randomFishResult() 改为读数据库事件

三、钓鱼参数迁移至 GameConfig
- fishing_cost/wait_min/wait_max/cooldown 改为 GameConfig::param() 读取
- 保留 Sysparam fallback 兼容旧数据

四、后台管理页面
- 新建 FishingEventController(CRUD + AJAX toggle)
- 新建 admin/fishing/index.blade.php(事件列表+概率显示+编辑弹窗)
- 侧边栏「游戏管理」下方新增「🎣 钓鱼事件」入口
- 游戏管理视图 gameParamLabels 新增钓鱼参数标签
This commit is contained in:
2026-03-03 16:46:36 +08:00
parent 783afe0677
commit 03ec3a9fbb
11 changed files with 735 additions and 50 deletions

View File

@@ -0,0 +1,108 @@
<?php
/**
* 文件功能:钓鱼事件后台管理控制器
* 提供钓鱼事件的列表展示、创建、编辑、删除、启用/禁用功能
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\FishingEvent;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class FishingEventController extends Controller
{
/**
* 显示所有钓鱼事件列表
*/
public function index(): View
{
$events = FishingEvent::orderBy('sort')->orderBy('id')->get();
$totalWeight = $events->where('is_active', true)->sum('weight');
return view('admin.fishing.index', compact('events', 'totalWeight'));
}
/**
* 创建新钓鱼事件
*/
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'emoji' => 'required|string|max:10',
'name' => 'required|string|max:100',
'message' => 'required|string|max:255',
'exp' => 'required|integer',
'jjb' => 'required|integer',
'weight' => 'required|integer|min:1|max:9999',
'sort' => 'required|integer|min:0',
'is_active' => 'boolean',
]);
$data['is_active'] = $request->boolean('is_active', true);
FishingEvent::create($data);
return redirect()->route('admin.fishing.index')->with('success', '钓鱼事件已添加!');
}
/**
* 更新钓鱼事件
*
* @param FishingEvent $fishing 路由模型绑定
*/
public function update(Request $request, FishingEvent $fishing): RedirectResponse
{
$data = $request->validate([
'emoji' => 'required|string|max:10',
'name' => 'required|string|max:100',
'message' => 'required|string|max:255',
'exp' => 'required|integer',
'jjb' => 'required|integer',
'weight' => 'required|integer|min:1|max:9999',
'sort' => 'required|integer|min:0',
'is_active' => 'boolean',
]);
$data['is_active'] = $request->boolean('is_active');
$fishing->update($data);
return redirect()->route('admin.fishing.index')->with('success', "事件「{$fishing->name}」已更新!");
}
/**
* 切换事件启用/禁用状态AJAX
*
* @param FishingEvent $fishing 路由模型绑定
*/
public function toggle(FishingEvent $fishing): JsonResponse
{
$fishing->update(['is_active' => ! $fishing->is_active]);
return response()->json([
'ok' => true,
'is_active' => $fishing->is_active,
'message' => $fishing->is_active ? "{$fishing->name}」已启用" : "{$fishing->name}」已禁用",
]);
}
/**
* 删除钓鱼事件
*
* @param FishingEvent $fishing 路由模型绑定
*/
public function destroy(FishingEvent $fishing): RedirectResponse
{
$name = $fishing->name;
$fishing->delete();
return redirect()->route('admin.fishing.index')->with('success', "事件「{$name}」已删除!");
}
}

View File

@@ -18,6 +18,8 @@ namespace App\Http\Controllers;
use App\Enums\CurrencySource; use App\Enums\CurrencySource;
use App\Events\MessageSent; use App\Events\MessageSent;
use App\Models\FishingEvent;
use App\Models\GameConfig;
use App\Models\Sysparam; use App\Models\Sysparam;
use App\Services\ChatStateService; use App\Services\ChatStateService;
use App\Services\ShopService; use App\Services\ShopService;
@@ -56,6 +58,11 @@ class FishingController extends Controller
return response()->json(['status' => 'error', 'message' => '请先登录'], 401); return response()->json(['status' => 'error', 'message' => '请先登录'], 401);
} }
// 检查钓鱼全局开关
if (! GameConfig::isEnabled('fishing')) {
return response()->json(['status' => 'error', 'message' => '钓鱼功能暂未开放。'], 403);
}
// 1. 检查冷却时间Redis TTL // 1. 检查冷却时间Redis TTL
$cooldownKey = "fishing:cd:{$user->id}"; $cooldownKey = "fishing:cd:{$user->id}";
if (Redis::exists($cooldownKey)) { if (Redis::exists($cooldownKey)) {
@@ -69,7 +76,7 @@ class FishingController extends Controller
} }
// 2. 检查金币是否足够 // 2. 检查金币是否足够
$cost = (int) Sysparam::getValue('fishing_cost', '5'); $cost = (int) (GameConfig::param('fishing', 'fishing_cost') ?? Sysparam::getValue('fishing_cost', '5'));
if (($user->jjb ?? 0) < $cost) { if (($user->jjb ?? 0) < $cost) {
return response()->json([ return response()->json([
'status' => 'error', 'status' => 'error',
@@ -87,8 +94,8 @@ class FishingController extends Controller
$user->refresh(); $user->refresh();
// 4. 生成一次性 token存入 RedisTTL = 等待时间 + 收竿窗口 + 缓冲) // 4. 生成一次性 token存入 RedisTTL = 等待时间 + 收竿窗口 + 缓冲)
$waitMin = (int) Sysparam::getValue('fishing_wait_min', '8'); $waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8'));
$waitMax = (int) Sysparam::getValue('fishing_wait_max', '15'); $waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15'));
$waitTime = rand($waitMin, $waitMax); $waitTime = rand($waitMin, $waitMax);
$token = Str::random(32); $token = Str::random(32);
$tokenKey = "fishing:token:{$user->id}"; $tokenKey = "fishing:token:{$user->id}";
@@ -169,7 +176,7 @@ class FishingController extends Controller
Redis::del($tokenKey); Redis::del($tokenKey);
// 2. 设置冷却时间 // 2. 设置冷却时间
$cooldown = (int) Sysparam::getValue('fishing_cooldown', '300'); $cooldown = (int) (GameConfig::param('fishing', 'fishing_cooldown') ?? Sysparam::getValue('fishing_cooldown', '300'));
Redis::setex("fishing:cd:{$user->id}", $cooldown, time()); Redis::setex("fishing:cd:{$user->id}", $cooldown, time());
// 3. 随机决定钓鱼结果 // 3. 随机决定钓鱼结果
@@ -229,57 +236,31 @@ class FishingController extends Controller
} }
/** /**
* 随机钓鱼结果(复刻原版概率分布 * 随机钓鱼结果(从数据库 fishing_events 加权随机抽取
*
* 若数据库中无激活事件,回退到兜底结果。
* *
* @return array{emoji: string, message: string, exp: int, jjb: int} * @return array{emoji: string, message: string, exp: int, jjb: int}
*/ */
private function randomFishResult(): array private function randomFishResult(): array
{ {
$roll = rand(1, 100); $event = FishingEvent::rollOne();
return match (true) { // 数据库无事件时的兜底
$roll <= 15 => [ if (! $event) {
'emoji' => '🦈', return [
'message' => '钓到一条大鲨鱼增加经验100、金币20', 'emoji' => '🐟',
'exp' => 100, 'message' => '钓到一条小鱼,获得金币10',
'jjb' => 20, 'exp' => 0,
], 'jjb' => 10,
$roll <= 30 => [ ];
'emoji' => '🐟', }
'message' => '钓到一条娃娃鱼到集市卖得30个金币',
'exp' => 0, return [
'jjb' => 30, 'emoji' => $event->emoji,
], 'message' => $event->message,
$roll <= 50 => [ 'exp' => $event->exp,
'emoji' => '🐠', 'jjb' => $event->jjb,
'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,
],
};
} }
} }

View File

@@ -0,0 +1,89 @@
<?php
/**
* 文件功能:钓鱼事件模型
*
* 对应 fishing_events 表,每条记录是一个钓鱼随机奖惩事件。
* 概率由 weight权重字段决定权重越大被选中概率越高。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class FishingEvent extends Model
{
/**
* 可批量赋值字段
*
* @var array<int, string>
*/
protected $fillable = [
'emoji',
'name',
'message',
'exp',
'jjb',
'weight',
'is_active',
'sort',
];
/**
* 字段类型转换
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'exp' => 'integer',
'jjb' => 'integer',
'weight' => 'integer',
'is_active' => 'boolean',
'sort' => 'integer',
];
}
/**
* 作用域:只查询已启用的事件
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
/**
* 根据权重随机抽取一个激活的钓鱼事件
*
* 实现加权随机:权重越大,被选中概率越高。
* 若无任何激活事件,返回 null
*/
public static function rollOne(): ?static
{
$events = static::active()->orderBy('sort')->get();
if ($events->isEmpty()) {
return null;
}
// 计算总权重后加权随机
$totalWeight = $events->sum('weight');
$roll = random_int(1, max($totalWeight, 1));
$cumulative = 0;
foreach ($events as $event) {
$cumulative += $event->weight;
if ($roll <= $cumulative) {
return $event;
}
}
// 兜底:返回最后一个
return $events->last();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\FishingEvent>
*/
class FishingEventFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* 文件功能:创建钓鱼事件表
*
* 将钓鱼随机事件从硬编码迁移到数据库,支持后台增删改。
* weight 字段为权重(不是百分比),由所有激活事件的权重之和决定概率。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 创建 fishing_events
*/
public function up(): void
{
Schema::create('fishing_events', function (Blueprint $table) {
$table->id();
$table->string('emoji', 10)->comment('事件表情符号');
$table->string('name', 100)->comment('事件名称(后台展示用)');
$table->string('message', 255)->comment('聊天室播报内容');
$table->integer('exp')->default(0)->comment('经验变化(正为获得,负为扣除)');
$table->integer('jjb')->default(0)->comment('金币变化(正为获得,负为扣除)');
$table->unsignedSmallInteger('weight')->default(10)->comment('权重(权重越大概率越高)');
$table->boolean('is_active')->default(true)->comment('是否启用');
$table->unsignedTinyInteger('sort')->default(0)->comment('排序(越小越靠前)');
$table->timestamps();
});
}
/**
* 回滚:删除 fishing_events
*/
public function down(): void
{
Schema::dropIfExists('fishing_events');
}
};

View File

@@ -0,0 +1,123 @@
<?php
/**
* 文件功能:钓鱼事件初始数据填充
*
* 将原硬编码的 7 个钓鱼事件迁移至数据库,
* 调整奖励:经验减少,金币提升,游戏更加休闲娱乐。
*
* 执行方式php artisan db:seed --class=FishingEventSeeder
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace Database\Seeders;
use App\Models\FishingEvent;
use Illuminate\Database\Seeder;
class FishingEventSeeder extends Seeder
{
/**
* 填充钓鱼事件数据
*
* weight 权重说明(总权重 100
* 15 大鲨鱼(幸运)
* 15 娃娃鱼(卖鱼)
* 20 大草鱼(经验)
* 20 小鲤鱼(双收)
* 15 掉河(惩罚)
* 10 被打(惩罚)
* 5 超级大奖
*/
public function run(): void
{
// 避免重复 Seed
if (FishingEvent::count() > 0) {
$this->command->info('钓鱼事件已存在,跳过填充。');
return;
}
$events = [
[
'sort' => 1,
'emoji' => '🦈',
'name' => '大鲨鱼',
'message' => '钓到一条大鲨鱼获得经验30、金币50',
'exp' => 30,
'jjb' => 50,
'weight' => 15,
'is_active' => true,
],
[
'sort' => 2,
'emoji' => '🐟',
'name' => '娃娃鱼',
'message' => '钓到一条娃娃鱼到集市卖得80个金币',
'exp' => 0,
'jjb' => 80,
'weight' => 15,
'is_active' => true,
],
[
'sort' => 3,
'emoji' => '🐠',
'name' => '大草鱼',
'message' => '钓到一只大草鱼吃下增加经验20、金币30',
'exp' => 20,
'jjb' => 30,
'weight' => 20,
'is_active' => true,
],
[
'sort' => 4,
'emoji' => '🐡',
'name' => '小鲤鱼',
'message' => '钓到一条小鲤鱼增加经验10、金币20',
'exp' => 10,
'jjb' => 20,
'weight' => 20,
'is_active' => true,
],
[
'sort' => 5,
'emoji' => '💧',
'name' => '落水惨败',
'message' => '鱼没钓到摔到河里损失金币30',
'exp' => 0,
'jjb' => -30,
'weight' => 15,
'is_active' => true,
],
[
'sort' => 6,
'emoji' => '👊',
'name' => '被抓殴打',
'message' => '偷钓鱼塘被主人发现一阵殴打金币减少10',
'exp' => 0,
'jjb' => -10,
'weight' => 10,
'is_active' => true,
],
[
'sort' => 7,
'emoji' => '🎉',
'name' => '超级大奖',
'message' => '运气爆棚!钓到大鲨鱼、大草鱼、小鲤鱼各一条!经验+50金币+200',
'exp' => 50,
'jjb' => 200,
'weight' => 5,
'is_active' => true,
],
];
foreach ($events as $event) {
FishingEvent::create($event);
}
$this->command->info('✅ 钓鱼事件已填充7 条)');
}
}

View File

@@ -116,6 +116,21 @@ class GameConfigSeeder extends Seeder
'curse_chance' => 5, // 大凶签概率(% 'curse_chance' => 5, // 大凶签概率(%
], ],
], ],
// ─── 钓鱼小游戏 ──────────────────────────────────────────────
[
'game_key' => 'fishing',
'name' => '钓鱼小游戏',
'icon' => '🎣',
'description' => '消耗金币抛竿,等待浮漂下沉后点击收竿,随机获得奖励或惩罚。持有自动钓鱼卡可自动循环。',
'enabled' => false,
'params' => [
'fishing_cost' => 5, // 每次抛竿消耗金币
'fishing_wait_min' => 8, // 浮漂等待最短秒数
'fishing_wait_max' => 15, // 浮漂等待最长秒数
'fishing_cooldown' => 300, // 收竿后冷却秒数
],
],
]; ];
foreach ($games as $game) { foreach ($games as $game) {

View File

@@ -0,0 +1,281 @@
@extends('admin.layouts.app')
@section('title', '钓鱼事件管理')
@section('content')
<div class="space-y-6">
{{-- 页头 --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex justify-between items-center">
<div>
<h2 class="text-lg font-bold text-gray-800">🎣 钓鱼事件管理</h2>
<p class="text-xs text-gray-500 mt-1">
管理钓鱼随机事件。权重越大被触发概率越高。
当前激活事件总权重:<strong class="text-indigo-600">{{ $totalWeight }}</strong>
</p>
</div>
<a href="{{ route('admin.game-configs.index') }}"
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-bold hover:bg-gray-200 transition">
⚙️ 钓鱼参数设置
</a>
</div>
{{-- Flash --}}
@if (session('success'))
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg text-sm">
{{ session('success') }}
</div>
@endif
{{-- 钓鱼事件列表 --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-100">
<tr>
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">排序</th>
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">符号</th>
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">名称</th>
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase w-1/3">播报内容</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">经验</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">金币</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">权重</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">概率</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">状态</th>
<th class="px-4 py-3 text-right text-xs font-bold text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@foreach ($events as $event)
<tr id="row-{{ $event->id }}" class="{{ $event->is_active ? '' : 'opacity-50' }}">
<td class="px-4 py-3 text-gray-500 text-xs">{{ $event->sort }}</td>
<td class="px-4 py-3 text-xl">{{ $event->emoji }}</td>
<td class="px-4 py-3 font-medium text-gray-800">{{ $event->name }}</td>
<td class="px-4 py-3 text-gray-500 text-xs">{{ $event->message }}</td>
<td
class="px-4 py-3 text-center font-mono text-xs
{{ $event->exp > 0 ? 'text-green-600' : ($event->exp < 0 ? 'text-red-500' : 'text-gray-400') }}">
{{ $event->exp > 0 ? '+' : '' }}{{ $event->exp }}
</td>
<td
class="px-4 py-3 text-center font-mono text-xs
{{ $event->jjb > 0 ? 'text-amber-600' : ($event->jjb < 0 ? 'text-red-500' : 'text-gray-400') }}">
{{ $event->jjb > 0 ? '+' : '' }}{{ $event->jjb }}
</td>
<td class="px-4 py-3 text-center text-xs text-indigo-600 font-bold">{{ $event->weight }}</td>
<td class="px-4 py-3 text-center text-xs text-gray-500">
@if ($totalWeight > 0 && $event->is_active)
{{ number_format(($event->weight / $totalWeight) * 100, 1) }}%
@else
@endif
</td>
<td class="px-4 py-3 text-center">
<button onclick="toggleEvent({{ $event->id }})" id="toggle-{{ $event->id }}"
class="px-2 py-1 rounded-full text-xs font-bold transition
{{ $event->is_active ? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' : 'bg-gray-100 text-gray-500 hover:bg-gray-200' }}">
{{ $event->is_active ? '启用' : '禁用' }}
</button>
</td>
<td class="px-4 py-3 text-right">
<button onclick="openEdit({{ $event->id }})"
class="px-3 py-1 bg-indigo-50 text-indigo-700 rounded text-xs font-bold hover:bg-indigo-100 transition mr-1">
编辑
</button>
<form action="{{ route('admin.fishing.destroy', $event->id) }}" method="POST"
class="inline" onsubmit="return confirm('确定删除事件「{{ $event->name }}」?')">
@csrf @method('DELETE')
<button type="submit"
class="px-3 py-1 bg-red-50 text-red-600 rounded text-xs font-bold hover:bg-red-100 transition">
删除
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- 新增事件卡片 --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-5 border-b border-gray-100 bg-gray-50">
<h3 class="font-bold text-gray-700 text-sm"> 新增钓鱼事件</h3>
</div>
<form action="{{ route('admin.fishing.store') }}" method="POST" class="p-5">
@csrf
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">Emoji 符号</label>
<input type="text" name="emoji" value="{{ old('emoji') }}" placeholder="🐟" required
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">名称</label>
<input type="text" name="name" value="{{ old('name') }}" placeholder="小鲤鱼" required
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">经验变化</label>
<input type="number" name="exp" value="{{ old('exp', 0) }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">金币变化</label>
<input type="number" name="jjb" value="{{ old('jjb', 0) }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div class="md:col-span-2">
<label class="block text-xs font-bold text-gray-600 mb-1">播报内容</label>
<input type="text" name="message" value="{{ old('message') }}" placeholder="钓到一条小鲤鱼..." required
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">权重(概率)</label>
<input type="number" name="weight" value="{{ old('weight', 10) }}" min="1"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">排序</label>
<input type="number" name="sort" value="{{ old('sort', 0) }}" min="0"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
</div>
<div class="mt-4 flex items-center gap-4">
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition text-sm shadow-sm">
💾 添加事件
</button>
<label class="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" name="is_active" value="1" checked class="rounded">
立即启用
</label>
</div>
</form>
</div>
</div>
{{-- 编辑弹窗 --}}
<div id="edit-modal" class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl w-full max-w-lg shadow-2xl">
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-bold text-gray-800">✏️ 编辑钓鱼事件</h3>
<button onclick="closeEdit()" class="text-gray-400 hover:text-gray-600 text-xl"></button>
</div>
<form id="edit-form" method="POST" class="p-5">
@csrf @method('PUT')
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">Emoji</label>
<input type="text" name="emoji" id="edit-emoji" required
class="w-full border border-gray-300 rounded-lg p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">名称</label>
<input type="text" name="name" id="edit-name" required
class="w-full border border-gray-300 rounded-lg p-2 text-sm">
</div>
<div class="col-span-2">
<label class="block text-xs font-bold text-gray-600 mb-1">播报内容</label>
<input type="text" name="message" id="edit-message" required
class="w-full border border-gray-300 rounded-lg p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">经验变化</label>
<input type="number" name="exp" id="edit-exp"
class="w-full border border-gray-300 rounded-lg p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">金币变化</label>
<input type="number" name="jjb" id="edit-jjb"
class="w-full border border-gray-300 rounded-lg p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">权重</label>
<input type="number" name="weight" id="edit-weight" min="1"
class="w-full border border-gray-300 rounded-lg p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">排序</label>
<input type="number" name="sort" id="edit-sort" min="0"
class="w-full border border-gray-300 rounded-lg p-2 text-sm">
</div>
<div class="col-span-2">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" name="is_active" id="edit-is-active" value="1" class="rounded">
启用此事件
</label>
</div>
</div>
<div class="mt-5 flex gap-3">
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition text-sm">
💾 保存修改
</button>
<button type="button" onclick="closeEdit()"
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-bold hover:bg-gray-200 transition text-sm">
取消
</button>
</div>
</form>
</div>
</div>
<script>
// 事件数据(供编辑弹窗填充)
const fishingEvents = @json($events->keyBy('id'));
/**
* 打开编辑弹窗并填充数据
*/
function openEdit(id) {
const e = fishingEvents[id];
if (!e) return;
document.getElementById('edit-form').action = `/admin/fishing/${id}`;
document.getElementById('edit-emoji').value = e.emoji;
document.getElementById('edit-name').value = e.name;
document.getElementById('edit-message').value = e.message;
document.getElementById('edit-exp').value = e.exp;
document.getElementById('edit-jjb').value = e.jjb;
document.getElementById('edit-weight').value = e.weight;
document.getElementById('edit-sort').value = e.sort;
document.getElementById('edit-is-active').checked = !!e.is_active;
document.getElementById('edit-modal').classList.remove('hidden');
}
/**
* 关闭编辑弹窗
*/
function closeEdit() {
document.getElementById('edit-modal').classList.add('hidden');
}
/**
* 切换事件启用/禁用状态
*/
function toggleEvent(id) {
fetch(`/admin/fishing/${id}/toggle`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
})
.then(r => r.json())
.then(data => {
if (!data.ok) return;
const btn = document.getElementById(`toggle-${id}`);
const row = document.getElementById(`row-${id}`);
btn.textContent = data.is_active ? '启用' : '禁用';
btn.className = `px-2 py-1 rounded-full text-xs font-bold transition ${
data.is_active
? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
: 'bg-gray-100 text-gray-500 hover:bg-gray-200'
}`;
row.classList.toggle('opacity-50', !data.is_active);
// 同步内存里的状态
if (fishingEvents[id]) fishingEvents[id].is_active = data.is_active;
});
}
</script>
@endsection

View File

@@ -248,6 +248,12 @@
'max' => 100, 'max' => 100,
], ],
], ],
'fishing' => [
'fishing_cost' => ['label' => '每次抛竿消耗', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'fishing_wait_min' => ['label' => '浮漂等待最短', 'type' => 'number', 'unit' => '秒', 'min' => 1],
'fishing_wait_max' => ['label' => '浮漂等待最长', 'type' => 'number', 'unit' => '秒', 'min' => 1],
'fishing_cooldown' => ['label' => '收竿后冷却时间', 'type' => 'number', 'unit' => '秒', 'min' => 10],
],
default => [], default => [],
}; };
} }

View File

@@ -82,6 +82,10 @@
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.game-configs.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}"> class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.game-configs.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '🎮 游戏管理' !!} {!! '🎮 游戏管理' !!}
</a> </a>
<a href="{{ route('admin.fishing.index') }}"
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.fishing.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '🎣 钓鱼事件' !!}
</a>
<a href="{{ route('admin.departments.index') }}" <a href="{{ route('admin.departments.index') }}"
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.departments.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}"> class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.departments.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '🏛️ 部门管理' !!} {!! '🏛️ 部门管理' !!}

View File

@@ -349,6 +349,15 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad
Route::post('/{gameConfig}/toggle', [\App\Http\Controllers\Admin\GameConfigController::class, 'toggle'])->name('toggle'); Route::post('/{gameConfig}/toggle', [\App\Http\Controllers\Admin\GameConfigController::class, 'toggle'])->name('toggle');
Route::post('/{gameConfig}/params', [\App\Http\Controllers\Admin\GameConfigController::class, 'updateParams'])->name('params'); Route::post('/{gameConfig}/params', [\App\Http\Controllers\Admin\GameConfigController::class, 'updateParams'])->name('params');
}); });
// 🎣 钓鱼事件管理
Route::prefix('fishing')->name('fishing.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\FishingEventController::class, 'index'])->name('index');
Route::post('/', [\App\Http\Controllers\Admin\FishingEventController::class, 'store'])->name('store');
Route::put('/{fishing}', [\App\Http\Controllers\Admin\FishingEventController::class, 'update'])->name('update');
Route::post('/{fishing}/toggle', [\App\Http\Controllers\Admin\FishingEventController::class, 'toggle'])->name('toggle');
Route::delete('/{fishing}', [\App\Http\Controllers\Admin\FishingEventController::class, 'destroy'])->name('destroy');
});
}); });
// ────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────