新增游戏管理系统:①game_configs表+模型(forGame/isEnabled/param静态方法) ②GameConfigSeeder初始化5款游戏参数 ③后台卡片式管理页(开关+参数表单) ④侧边栏菜单「游戏管理」

This commit is contained in:
2026-03-01 20:17:18 +08:00
parent 8c99e1fad7
commit 8a74bfd639
7 changed files with 585 additions and 0 deletions
@@ -0,0 +1,254 @@
@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">
<h2 class="text-lg font-bold text-gray-800">🎮 游戏管理</h2>
<p class="text-xs text-gray-500 mt-1">统一管理聊天室所有娱乐游戏的开关状态与核心参数,所有游戏默认关闭。</p>
</div>
@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="grid grid-cols-1 gap-6">
@foreach ($games as $game)
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
id="game-card-{{ $game->game_key }}">
{{-- 卡片头部:游戏名 + 开关 --}}
<div
class="flex items-center justify-between p-5 border-b border-gray-100
{{ $game->enabled ? 'bg-emerald-50' : 'bg-gray-50' }}">
<div class="flex items-center gap-3">
<span class="text-3xl">{{ $game->icon }}</span>
<div>
<div class="font-bold text-gray-800 flex items-center gap-2">
{{ $game->name }}
<span id="badge-{{ $game->game_key }}"
class="text-xs px-2 py-0.5 rounded-full font-bold
{{ $game->enabled ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-200 text-gray-500' }}">
{{ $game->enabled ? '运行中' : '已关闭' }}
</span>
</div>
<div class="text-xs text-gray-500 mt-0.5">{{ $game->description }}</div>
</div>
</div>
{{-- 大开关按钮 --}}
<button onclick="toggleGame('{{ $game->game_key }}', {{ $game->id }})"
id="toggle-btn-{{ $game->game_key }}"
class="px-5 py-2 rounded-lg font-bold text-sm transition shadow-sm
{{ $game->enabled ? 'bg-red-500 hover:bg-red-600 text-white' : 'bg-emerald-500 hover:bg-emerald-600 text-white' }}">
{{ $game->enabled ? '⏸ 关闭游戏' : '▶ 开启游戏' }}
</button>
</div>
{{-- 参数配置区域 --}}
<div class="p-5">
<form action="{{ route('admin.game-configs.params', $game) }}" method="POST">
@csrf
@php
$params = $game->params ?? [];
$labels = gameParamLabels($game->game_key);
@endphp
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
@foreach ($params as $paramKey => $paramValue)
@php $meta = $labels[$paramKey] ?? ['label' => $paramKey, 'type' => 'number', 'unit' => ''] @endphp
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">
{{ $meta['label'] }}
@if ($meta['unit'])
<span class="font-normal text-gray-400">{{ $meta['unit'] }}</span>
@endif
</label>
@if ($meta['type'] === 'boolean')
<select name="params[{{ $paramKey }}]"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
<option value="1" {{ $paramValue ? 'selected' : '' }}></option>
<option value="0" {{ !$paramValue ? 'selected' : '' }}></option>
</select>
@elseif ($meta['type'] === 'array')
<input type="text" name="params[{{ $paramKey }}]"
value="{{ implode(',', (array) $paramValue) }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400"
placeholder="多个值用逗号分隔">
@else
<input type="{{ $meta['type'] }}" name="params[{{ $paramKey }}]"
value="{{ $paramValue }}" step="{{ $meta['step'] ?? 1 }}"
min="{{ $meta['min'] ?? 0 }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
@endif
</div>
@endforeach
</div>
<div class="mt-4 flex items-center gap-3">
<button type="submit"
class="px-6 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition text-sm shadow-sm">
💾 保存参数
</button>
<span class="text-xs text-gray-400">修改后立即生效(缓存60秒刷新)</span>
</div>
</form>
</div>
</div>
@endforeach
@if ($games->isEmpty())
<div class="bg-white rounded-xl border border-gray-100 p-12 text-center text-gray-400">
暂无游戏配置,请先运行 <code class="bg-gray-100 px-2 py-1 rounded">php artisan db:seed
--class=GameConfigSeeder</code>
</div>
@endif
</div>
</div>
<script>
/**
* 切换游戏开启/关闭状态
*/
function toggleGame(gameKey, gameId) {
fetch(`/admin/game-configs/${gameId}/toggle`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
})
.then(r => r.json())
.then(data => {
if (!data.ok) return;
const enabled = data.enabled;
const card = document.getElementById(`game-card-${gameKey}`);
const badge = document.getElementById(`badge-${gameKey}`);
const btn = document.getElementById(`toggle-btn-${gameKey}`);
const header = card?.querySelector('.flex.items-center.justify-between');
// 更新徽章
badge.textContent = enabled ? '运行中' : '已关闭';
badge.className = `text-xs px-2 py-0.5 rounded-full font-bold ${enabled
? 'bg-emerald-100 text-emerald-700'
: 'bg-gray-200 text-gray-500'}`;
// 更新按钮
btn.textContent = enabled ? '⏸ 关闭游戏' : '▶ 开启游戏';
btn.className = `px-5 py-2 rounded-lg font-bold text-sm transition shadow-sm ${enabled
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-emerald-500 hover:bg-emerald-600 text-white'}`;
// 更新头部背景
if (header) {
header.classList.toggle('bg-emerald-50', enabled);
header.classList.toggle('bg-gray-50', !enabled);
}
// Toast 提示
alert(data.message);
});
}
</script>
@endsection
@php
/**
* 返回各游戏参数的中文标签说明。
*
* @param string $gameKey
* @return array<string, array{label: string, type: string, unit: string}>
*/
function gameParamLabels(string $gameKey): array
{
return match ($gameKey) {
'baccarat' => [
'interval_minutes' => ['label' => '开局间隔', 'type' => 'number', 'unit' => '分钟', 'min' => 1],
'bet_window_seconds' => ['label' => '押注窗口', 'type' => 'number', 'unit' => '秒', 'min' => 10],
'min_bet' => ['label' => '最低押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'max_bet' => ['label' => '最高押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'payout_big' => ['label' => '大赔率(1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'payout_small' => ['label' => '小赔率(1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'payout_triple' => ['label' => '豹子赔率(1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'kill_points' => ['label' => '庄家收割点数', 'type' => 'array', 'unit' => '逗号分隔'],
],
'slot_machine' => [
'cost_per_spin' => ['label' => '每次旋转消耗', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'house_edge_percent' => [
'label' => '庄家边际',
'type' => 'number',
'unit' => '%',
'min' => 0,
'step' => 0.1,
],
'daily_limit' => ['label' => '每日转动上限', 'type' => 'number', 'unit' => '次(0=不限)', 'min' => 0],
'jackpot_payout' => ['label' => '三个7赔率(1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'triple_payout' => ['label' => '三💎赔率(1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'same_payout' => ['label' => '其他三同(1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'pair_payout' => ['label' => '两同赔率(1:N', 'type' => 'number', 'unit' => '', 'min' => 1],
'curse_enabled' => ['label' => '开启诅咒(三💀)', 'type' => 'boolean', 'unit' => ''],
],
'mystery_box' => [
'auto_drop_enabled' => ['label' => '自动定时投放', 'type' => 'boolean', 'unit' => ''],
'auto_interval_hours' => ['label' => '自动投放间隔', 'type' => 'number', 'unit' => '小时', 'min' => 1],
'claim_window_seconds' => ['label' => '领取窗口', 'type' => 'number', 'unit' => '秒', 'min' => 10],
'min_reward' => ['label' => '普通箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'max_reward' => ['label' => '普通箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'rare_min_reward' => ['label' => '稀有箱最低奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'rare_max_reward' => ['label' => '稀有箱最高奖励', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'trap_chance_percent' => [
'label' => '黑化箱概率',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 100,
],
],
'horse_racing' => [
'interval_minutes' => ['label' => '比赛间隔', 'type' => 'number', 'unit' => '分钟', 'min' => 5],
'bet_window_seconds' => ['label' => '押注窗口', 'type' => 'number', 'unit' => '秒', 'min' => 10],
'race_duration' => ['label' => '跑马动画时长', 'type' => 'number', 'unit' => '秒', 'min' => 10],
'horse_count' => ['label' => '参赛马匹数', 'type' => 'number', 'unit' => '匹', 'min' => 2, 'max' => 8],
'min_bet' => ['label' => '最低押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'max_bet' => ['label' => '最高押注', 'type' => 'number', 'unit' => '金币', 'min' => 1],
'house_take_percent' => [
'label' => '庄家抽水',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 20,
],
],
'fortune_telling' => [
'free_count_per_day' => ['label' => '每日免费次数', 'type' => 'number', 'unit' => '次', 'min' => 0],
'extra_cost' => ['label' => '额外次数消耗', 'type' => 'number', 'unit' => '金币', 'min' => 0],
'buff_duration_hours' => ['label' => '加成持续时间', 'type' => 'number', 'unit' => '小时', 'min' => 1],
'jackpot_chance' => [
'label' => '上上签概率',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 100,
],
'good_chance' => ['label' => '上签概率', 'type' => 'number', 'unit' => '%', 'min' => 0, 'max' => 100],
'bad_chance' => ['label' => '下签概率', 'type' => 'number', 'unit' => '%', 'min' => 0, 'max' => 100],
'curse_chance' => [
'label' => '大凶签概率',
'type' => 'number',
'unit' => '%',
'min' => 0,
'max' => 100,
],
],
default => [],
};
}
@endphp
@@ -79,6 +79,10 @@
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.holiday-events.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '🎊 节日福利' . $ro !!}
</a>
<a href="{{ route('admin.game-configs.index') }}"
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.game-configs.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '🎮 游戏管理' . $ro !!}
</a>
<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' }}">
{!! '🏛️ 部门管理' . $ro !!}