一、钓鱼全局开关
- 钓鱼纳入 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 新增钓鱼参数标签
282 lines
16 KiB
PHP
282 lines
16 KiB
PHP
@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
|